@tradejs/cli 3.1.29-beta.291 → 3.1.29-beta.292
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/cli.js +10497 -9777
- package/dist/lib/coreResearch.js +261 -115
- package/dist/lib/researchVisuals.js +27 -6
- package/dist/lib/strategyResearch.js +202 -167
- package/dist/scripts/aiPocketSearch.js +3343 -3029
- package/dist/scripts/coreResearch.js +263 -115
- package/dist/scripts/replay.js +480 -111
- package/dist/scripts/replayRunner.js +480 -111
- package/dist/scripts/researchVisuals.js +27 -6
- package/dist/scripts/strategyResearch.js +204 -168
- package/package.json +7 -7
package/dist/lib/coreResearch.js
CHANGED
|
@@ -52,17 +52,168 @@ var import_node_crypto5 = require("crypto");
|
|
|
52
52
|
var import_coreResearch = require("@tradejs/infra/coreResearch");
|
|
53
53
|
|
|
54
54
|
// src/lib/coreResearch/artifacts.ts
|
|
55
|
-
var
|
|
55
|
+
var import_node_fs3 = require("fs");
|
|
56
56
|
var import_node_events = require("events");
|
|
57
57
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
58
|
-
var
|
|
58
|
+
var import_node_path3 = __toESM(require("path"));
|
|
59
59
|
|
|
60
60
|
// src/lib/coreResearch/io.ts
|
|
61
61
|
var import_node_crypto = require("crypto");
|
|
62
|
-
var
|
|
62
|
+
var import_node_fs2 = require("fs");
|
|
63
63
|
var import_promises = __toESM(require("fs/promises"));
|
|
64
64
|
var import_backtest = require("@tradejs/core/backtest");
|
|
65
|
+
var import_node_path2 = __toESM(require("path"));
|
|
66
|
+
|
|
67
|
+
// src/lib/coreResearch/trace.ts
|
|
68
|
+
var import_node_fs = require("fs");
|
|
69
|
+
var import_node_readline = __toESM(require("readline"));
|
|
65
70
|
var import_node_path = __toESM(require("path"));
|
|
71
|
+
async function* readCoreResearchTrace(filePaths) {
|
|
72
|
+
for (const inputPath of filePaths) {
|
|
73
|
+
const filePath = import_node_path.default.resolve(inputPath);
|
|
74
|
+
const input = (0, import_node_fs.createReadStream)(filePath);
|
|
75
|
+
const lines = import_node_readline.default.createInterface({ input, crlfDelay: Infinity });
|
|
76
|
+
let index = -1;
|
|
77
|
+
try {
|
|
78
|
+
for await (const line of lines) {
|
|
79
|
+
index++;
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
let event;
|
|
82
|
+
try {
|
|
83
|
+
event = JSON.parse(line);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
yield event;
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
lines.close();
|
|
93
|
+
input.destroy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var summarizeCoreResearchTrace = async (filePaths = []) => {
|
|
98
|
+
const events = /* @__PURE__ */ Object.create(null);
|
|
99
|
+
const skipCounts = /* @__PURE__ */ Object.create(null);
|
|
100
|
+
const exitCodes = /* @__PURE__ */ Object.create(null);
|
|
101
|
+
const policyRejections = {};
|
|
102
|
+
const policyExamples = [];
|
|
103
|
+
for await (const event of readCoreResearchTrace(filePaths)) {
|
|
104
|
+
events[event.event] = (events[event.event] ?? 0) + 1;
|
|
105
|
+
if (event.event === "skip_summary") {
|
|
106
|
+
for (const [reason, count] of Object.entries(event.skipCounts)) {
|
|
107
|
+
skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (event.event === "position_exited" && event.exitCode) {
|
|
111
|
+
exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
|
|
112
|
+
}
|
|
113
|
+
if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
|
|
114
|
+
const reason = event.researchEntryDecision.reason;
|
|
115
|
+
policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
|
|
116
|
+
if (policyExamples.length < 20)
|
|
117
|
+
policyExamples.push({
|
|
118
|
+
timestamp: event.timestamp,
|
|
119
|
+
symbol: event.symbol,
|
|
120
|
+
decision: event.researchEntryDecision
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
events: Object.fromEntries(Object.entries(events).sort()),
|
|
126
|
+
skipCounts: Object.fromEntries(
|
|
127
|
+
Object.entries(skipCounts).sort(
|
|
128
|
+
([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
exitCodes: Object.fromEntries(
|
|
132
|
+
Object.entries(exitCodes).sort(
|
|
133
|
+
([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
...policyExamples.length ? {
|
|
137
|
+
researchPolicy: {
|
|
138
|
+
rejections: policyRejections,
|
|
139
|
+
examples: policyExamples
|
|
140
|
+
}
|
|
141
|
+
} : {}
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/lib/coreResearch/support.ts
|
|
146
|
+
async function assessCoreResearchSupport(spec, criteria) {
|
|
147
|
+
const symbols = new Set(spec.universe.symbols);
|
|
148
|
+
const blockMs = spec.robustness.clusterDays * 864e5;
|
|
149
|
+
const variants = [];
|
|
150
|
+
for (const variant of spec.variants) {
|
|
151
|
+
if (!variant.traceFiles?.length) continue;
|
|
152
|
+
const setups = /* @__PURE__ */ new Set();
|
|
153
|
+
const timestamps = /* @__PURE__ */ new Set();
|
|
154
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
155
|
+
const seenSymbols = /* @__PURE__ */ new Set();
|
|
156
|
+
const runs = /* @__PURE__ */ new Set();
|
|
157
|
+
let fallbackRows = 0;
|
|
158
|
+
for await (const event of readCoreResearchTrace(variant.traceFiles)) {
|
|
159
|
+
if (variant.runId && event.backtestRunId !== variant.runId) continue;
|
|
160
|
+
runs.add(event.backtestRunId);
|
|
161
|
+
if (event.event !== "signal_emitted" && event.event !== "entry_rejected" || event.strategy !== spec.strategy || !symbols.has(event.symbol) || spec.hypothesis.target !== "ALL" && event.direction !== spec.hypothesis.target || !Number.isSafeInteger(event.timestamp) || event.timestamp < spec.window.start || event.timestamp >= spec.window.end)
|
|
162
|
+
continue;
|
|
163
|
+
timestamps.add(event.timestamp);
|
|
164
|
+
seenSymbols.add(event.symbol);
|
|
165
|
+
if (event.setupIdentitySource === "strategy-context" && event.setupIdentity) {
|
|
166
|
+
setups.add(
|
|
167
|
+
JSON.stringify([event.symbol, event.direction, event.setupIdentity])
|
|
168
|
+
);
|
|
169
|
+
} else fallbackRows++;
|
|
170
|
+
const block = Math.floor((event.timestamp - spec.window.start) / blockMs);
|
|
171
|
+
const times = blocks.get(block) ?? /* @__PURE__ */ new Set();
|
|
172
|
+
times.add(event.timestamp);
|
|
173
|
+
blocks.set(block, times);
|
|
174
|
+
}
|
|
175
|
+
if (runs.size > 1)
|
|
176
|
+
throw new Error(`Support traces mix runs for ${variant.id}`);
|
|
177
|
+
variants.push({
|
|
178
|
+
id: variant.id,
|
|
179
|
+
role: variant.role,
|
|
180
|
+
setups,
|
|
181
|
+
events: timestamps.size,
|
|
182
|
+
symbols: seenSymbols.size,
|
|
183
|
+
fallbackRows,
|
|
184
|
+
blocks: [...blocks].sort(([a], [b]) => a - b).map(([index, times]) => ({
|
|
185
|
+
start: spec.window.start + index * blockMs,
|
|
186
|
+
events: times.size
|
|
187
|
+
})),
|
|
188
|
+
passed: timestamps.size >= criteria.minimumEvents && blocks.size >= criteria.minimumBlocks
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const control = variants.find((v) => v.role === "control");
|
|
192
|
+
const candidates = variants.filter((v) => v.role === "candidate");
|
|
193
|
+
const comparisons = control ? candidates.map((candidate) => ({
|
|
194
|
+
candidate: candidate.id,
|
|
195
|
+
commonSetups: [...candidate.setups].filter(
|
|
196
|
+
(id) => control.setups.has(id)
|
|
197
|
+
).length
|
|
198
|
+
})) : [];
|
|
199
|
+
const missing = !control || criteria.minimumCommonSetups !== void 0 && candidates.length !== spec.variants.filter((v) => v.role === "candidate").length;
|
|
200
|
+
const insufficient = variants.some((v) => !v.passed) || comparisons.some(
|
|
201
|
+
(c) => c.commonSetups < (criteria.minimumCommonSetups ?? 0)
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
schema: "tradejs-core-research-support/v1",
|
|
205
|
+
window: { start: spec.window.start, end: spec.window.end },
|
|
206
|
+
target: spec.hypothesis.target,
|
|
207
|
+
blockDays: spec.robustness.clusterDays,
|
|
208
|
+
criteria,
|
|
209
|
+
status: missing ? "missing-evidence" : insufficient ? "insufficient-support" : "supported",
|
|
210
|
+
variants: variants.map(({ setups, ...variant }) => ({
|
|
211
|
+
...variant,
|
|
212
|
+
explicitSetups: setups.size
|
|
213
|
+
})),
|
|
214
|
+
comparisons
|
|
215
|
+
};
|
|
216
|
+
}
|
|
66
217
|
|
|
67
218
|
// src/lib/coreResearch/types.ts
|
|
68
219
|
var CORE_RESEARCH_SCHEMA = "tradejs-core-research/v1";
|
|
@@ -88,7 +239,7 @@ var sha256Text = (value) => (0, import_node_crypto.createHash)("sha256").update(
|
|
|
88
239
|
var sha256Json = (value) => sha256Text(canonicalJson(value));
|
|
89
240
|
var sha256File = async (filePath) => {
|
|
90
241
|
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
91
|
-
const input = (0,
|
|
242
|
+
const input = (0, import_node_fs2.createReadStream)(filePath);
|
|
92
243
|
for await (const chunk of input) hash.update(chunk);
|
|
93
244
|
return hash.digest("hex");
|
|
94
245
|
};
|
|
@@ -327,33 +478,49 @@ var validateCoreResearchSpec = (value) => {
|
|
|
327
478
|
Boolean(spec.artifacts?.ledgerPath?.trim()),
|
|
328
479
|
"artifacts.ledgerPath is required"
|
|
329
480
|
);
|
|
481
|
+
if (spec.support !== void 0) {
|
|
482
|
+
assert(
|
|
483
|
+
spec.support && typeof spec.support === "object",
|
|
484
|
+
"support must be an object"
|
|
485
|
+
);
|
|
486
|
+
for (const [key, value2] of [
|
|
487
|
+
["minimumEvents", spec.support.minimumEvents],
|
|
488
|
+
["minimumBlocks", spec.support.minimumBlocks],
|
|
489
|
+
...spec.support.minimumCommonSetups === void 0 ? [] : [["minimumCommonSetups", spec.support.minimumCommonSetups]]
|
|
490
|
+
]) {
|
|
491
|
+
assert(
|
|
492
|
+
typeof value2 === "number" && Number.isSafeInteger(value2) && value2 > 0,
|
|
493
|
+
`support.${key} must be a positive integer`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
330
497
|
return spec;
|
|
331
498
|
};
|
|
332
499
|
var loadCoreResearchSpec = async (specPath) => {
|
|
333
|
-
const resolvedPath =
|
|
500
|
+
const resolvedPath = import_node_path2.default.resolve(specPath);
|
|
334
501
|
const parsed = JSON.parse(await import_promises.default.readFile(resolvedPath, "utf8"));
|
|
335
502
|
const spec = validateCoreResearchSpec(parsed);
|
|
336
503
|
return { resolvedPath, spec, specSha256: sha256Json(spec) };
|
|
337
504
|
};
|
|
338
505
|
var resolveCoreResearchPaths = (spec) => {
|
|
339
|
-
const rootDir =
|
|
340
|
-
const researchDir =
|
|
506
|
+
const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
|
|
507
|
+
const researchDir = import_node_path2.default.join(rootDir, spec.researchId);
|
|
341
508
|
return {
|
|
342
509
|
rootDir,
|
|
343
510
|
researchDir,
|
|
344
|
-
ledgerPath:
|
|
345
|
-
specPath:
|
|
346
|
-
manifestPath:
|
|
347
|
-
resultPath:
|
|
348
|
-
comparisonPath:
|
|
349
|
-
matchesPath:
|
|
350
|
-
tradesPath:
|
|
351
|
-
reportPath:
|
|
352
|
-
runLogDir:
|
|
511
|
+
ledgerPath: import_node_path2.default.resolve(spec.artifacts.ledgerPath),
|
|
512
|
+
specPath: import_node_path2.default.join(researchDir, "spec.json"),
|
|
513
|
+
manifestPath: import_node_path2.default.join(researchDir, "manifest.json"),
|
|
514
|
+
resultPath: import_node_path2.default.join(researchDir, "result.json"),
|
|
515
|
+
comparisonPath: import_node_path2.default.join(researchDir, "comparison.json"),
|
|
516
|
+
matchesPath: import_node_path2.default.join(researchDir, "matches.csv"),
|
|
517
|
+
tradesPath: import_node_path2.default.join(researchDir, "trades.jsonl"),
|
|
518
|
+
reportPath: import_node_path2.default.join(researchDir, "report.html"),
|
|
519
|
+
runLogDir: import_node_path2.default.join(researchDir, "runs")
|
|
353
520
|
};
|
|
354
521
|
};
|
|
355
522
|
var writeJsonAtomic = async (filePath, value) => {
|
|
356
|
-
await import_promises.default.mkdir(
|
|
523
|
+
await import_promises.default.mkdir(import_node_path2.default.dirname(filePath), { recursive: true });
|
|
357
524
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
358
525
|
await import_promises.default.writeFile(temporaryPath, `${canonicalJson(value)}
|
|
359
526
|
`, "utf8");
|
|
@@ -400,7 +567,7 @@ var verifyCoreResearchLedger = (records) => {
|
|
|
400
567
|
};
|
|
401
568
|
var appendCoreResearchLedger = async (params) => {
|
|
402
569
|
const { ledgerPath } = params;
|
|
403
|
-
await import_promises.default.mkdir(
|
|
570
|
+
await import_promises.default.mkdir(import_node_path2.default.dirname(ledgerPath), { recursive: true });
|
|
404
571
|
const lockPath = `${ledgerPath}.lock`;
|
|
405
572
|
let lock = null;
|
|
406
573
|
try {
|
|
@@ -439,14 +606,14 @@ var appendCoreResearchLedger = async (params) => {
|
|
|
439
606
|
var validateCoreResearchParentLineage = async (spec) => {
|
|
440
607
|
if (spec.stage === "screen") return;
|
|
441
608
|
const expectedParentStage = spec.stage === "isolated_long" ? "screen" : "isolated_long";
|
|
442
|
-
const rootDir =
|
|
609
|
+
const rootDir = import_node_path2.default.resolve(spec.artifacts.rootDir);
|
|
443
610
|
const passedParentCandidateConfigShas = /* @__PURE__ */ new Set();
|
|
444
611
|
for (const parentResearchId of spec.parentResearchIds ?? []) {
|
|
445
|
-
const parentDir =
|
|
612
|
+
const parentDir = import_node_path2.default.join(rootDir, parentResearchId);
|
|
446
613
|
const [parentSpec, parentManifest, parentResult] = await Promise.all([
|
|
447
|
-
import_promises.default.readFile(
|
|
448
|
-
import_promises.default.readFile(
|
|
449
|
-
import_promises.default.readFile(
|
|
614
|
+
import_promises.default.readFile(import_node_path2.default.join(parentDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
|
|
615
|
+
import_promises.default.readFile(import_node_path2.default.join(parentDir, "manifest.json"), "utf8").then((text) => JSON.parse(text)),
|
|
616
|
+
import_promises.default.readFile(import_node_path2.default.join(parentDir, "result.json"), "utf8").then((text) => JSON.parse(text))
|
|
450
617
|
]);
|
|
451
618
|
validateCoreResearchSpec(parentSpec);
|
|
452
619
|
if (parentManifest.status !== "completed") {
|
|
@@ -520,15 +687,46 @@ var prepareCoreResearch = async (spec) => {
|
|
|
520
687
|
return { paths, specSha256, manifest: existingManifest };
|
|
521
688
|
}
|
|
522
689
|
}
|
|
690
|
+
const supportSources = spec.support ? await Promise.all(
|
|
691
|
+
[
|
|
692
|
+
...new Set(
|
|
693
|
+
spec.variants.flatMap((variant) => variant.traceFiles ?? [])
|
|
694
|
+
)
|
|
695
|
+
].map(async (file) => ({
|
|
696
|
+
file: import_node_path2.default.resolve(file),
|
|
697
|
+
sha256: await sha256File(file).catch((error) => {
|
|
698
|
+
if (error.code === "ENOENT") return null;
|
|
699
|
+
throw error;
|
|
700
|
+
})
|
|
701
|
+
}))
|
|
702
|
+
) : [];
|
|
703
|
+
const missingSources = supportSources.filter(
|
|
704
|
+
(source) => source.sha256 === null
|
|
705
|
+
);
|
|
706
|
+
const support = spec.support ? missingSources.length ? {
|
|
707
|
+
schema: "tradejs-core-research-support/v1",
|
|
708
|
+
status: "missing-evidence",
|
|
709
|
+
criteria: spec.support
|
|
710
|
+
} : await assessCoreResearchSupport(spec, spec.support) : void 0;
|
|
711
|
+
const supportPath = import_node_path2.default.join(paths.researchDir, "support.json");
|
|
712
|
+
if (support) {
|
|
713
|
+
for (const source of supportSources) {
|
|
714
|
+
if (source.sha256 !== null && source.sha256 !== await sha256File(source.file))
|
|
715
|
+
throw new Error("Support trace changed during preparation");
|
|
716
|
+
}
|
|
717
|
+
await writeJsonAtomic(supportPath, { ...support, sources: supportSources });
|
|
718
|
+
}
|
|
523
719
|
await writeJsonAtomic(paths.specPath, spec);
|
|
524
720
|
const manifest = {
|
|
525
721
|
schema: "tradejs-core-research-manifest/v1",
|
|
526
722
|
researchId: spec.researchId,
|
|
527
723
|
specSha256,
|
|
528
724
|
status: "prepared",
|
|
725
|
+
...support ? { support: support.status } : {},
|
|
529
726
|
createdAt: spec.createdAt,
|
|
530
727
|
artifactHashes: {
|
|
531
|
-
"spec.json": await sha256File(paths.specPath)
|
|
728
|
+
"spec.json": await sha256File(paths.specPath),
|
|
729
|
+
...support ? { "support.json": await sha256File(supportPath) } : {}
|
|
532
730
|
}
|
|
533
731
|
};
|
|
534
732
|
await writeJsonAtomic(paths.manifestPath, manifest);
|
|
@@ -547,6 +745,24 @@ var prepareCoreResearch = async (spec) => {
|
|
|
547
745
|
}
|
|
548
746
|
return { paths, specSha256, manifest };
|
|
549
747
|
};
|
|
748
|
+
var verifyPreparedSupport = async (prepared) => {
|
|
749
|
+
if (!prepared.manifest.support) return;
|
|
750
|
+
if (prepared.manifest.support !== "supported") {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Research preflight: ${prepared.manifest.support}; see support.json`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const file = import_node_path2.default.join(prepared.paths.researchDir, "support.json");
|
|
756
|
+
const content = await import_promises.default.readFile(file);
|
|
757
|
+
if ((0, import_node_crypto.createHash)("sha256").update(content).digest("hex") !== prepared.manifest.artifactHashes["support.json"]) {
|
|
758
|
+
throw new Error("Support report checksum mismatch");
|
|
759
|
+
}
|
|
760
|
+
const { sources } = JSON.parse(content.toString());
|
|
761
|
+
for (const source of sources) {
|
|
762
|
+
if (source.sha256 !== await sha256File(source.file))
|
|
763
|
+
throw new Error("Support trace changed after preparation");
|
|
764
|
+
}
|
|
765
|
+
};
|
|
550
766
|
var verifyCoreResearchArtifacts = async (spec) => {
|
|
551
767
|
const paths = resolveCoreResearchPaths(spec);
|
|
552
768
|
const manifest = JSON.parse(
|
|
@@ -559,7 +775,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
|
|
|
559
775
|
manifest.artifactHashes
|
|
560
776
|
)) {
|
|
561
777
|
const actualHash = await sha256File(
|
|
562
|
-
|
|
778
|
+
import_node_path2.default.join(paths.researchDir, relativePath)
|
|
563
779
|
);
|
|
564
780
|
if (actualHash !== expectedHash) {
|
|
565
781
|
throw new Error(`Artifact hash mismatch: ${relativePath}`);
|
|
@@ -575,7 +791,7 @@ var verifyCoreResearchArtifacts = async (spec) => {
|
|
|
575
791
|
};
|
|
576
792
|
};
|
|
577
793
|
var scanCoreResearchStages = async (rootDir) => {
|
|
578
|
-
const resolvedRoot =
|
|
794
|
+
const resolvedRoot = import_node_path2.default.resolve(rootDir);
|
|
579
795
|
let entries = [];
|
|
580
796
|
try {
|
|
581
797
|
entries = await import_promises.default.readdir(resolvedRoot);
|
|
@@ -593,15 +809,15 @@ var scanCoreResearchStages = async (rootDir) => {
|
|
|
593
809
|
for (const entry of entries.sort(
|
|
594
810
|
(left, right) => left < right ? -1 : left > right ? 1 : 0
|
|
595
811
|
)) {
|
|
596
|
-
const researchDir =
|
|
812
|
+
const researchDir = import_node_path2.default.join(resolvedRoot, entry);
|
|
597
813
|
const stats = await import_promises.default.stat(researchDir);
|
|
598
814
|
if (!stats.isDirectory()) continue;
|
|
599
815
|
let spec;
|
|
600
816
|
let manifest;
|
|
601
817
|
try {
|
|
602
818
|
[spec, manifest] = await Promise.all([
|
|
603
|
-
import_promises.default.readFile(
|
|
604
|
-
import_promises.default.readFile(
|
|
819
|
+
import_promises.default.readFile(import_node_path2.default.join(researchDir, "spec.json"), "utf8").then((text) => JSON.parse(text)),
|
|
820
|
+
import_promises.default.readFile(import_node_path2.default.join(researchDir, "manifest.json"), "utf8").then((text) => JSON.parse(text))
|
|
605
821
|
]);
|
|
606
822
|
} catch (error) {
|
|
607
823
|
if (error.code === "ENOENT") continue;
|
|
@@ -611,7 +827,7 @@ var scanCoreResearchStages = async (rootDir) => {
|
|
|
611
827
|
if (manifest.specSha256 !== sha256Json(spec)) {
|
|
612
828
|
throw new Error(`Stage index found a spec hash mismatch for ${entry}`);
|
|
613
829
|
}
|
|
614
|
-
const result = manifest.status === "completed" ? await import_promises.default.readFile(
|
|
830
|
+
const result = manifest.status === "completed" ? await import_promises.default.readFile(import_node_path2.default.join(researchDir, "result.json"), "utf8").then((text) => JSON.parse(text)) : null;
|
|
615
831
|
familyByResearch.set(spec.researchId, spec.hypothesis.family);
|
|
616
832
|
experiments.push({
|
|
617
833
|
researchId: spec.researchId,
|
|
@@ -642,7 +858,7 @@ var scanCoreResearchStages = async (rootDir) => {
|
|
|
642
858
|
return { experiments, familyByResearch };
|
|
643
859
|
};
|
|
644
860
|
var writeCoreResearchStageIndex = async (rootDir) => {
|
|
645
|
-
const resolvedRoot =
|
|
861
|
+
const resolvedRoot = import_node_path2.default.resolve(rootDir);
|
|
646
862
|
const { experiments, familyByResearch } = await scanCoreResearchStages(resolvedRoot);
|
|
647
863
|
const grouped = /* @__PURE__ */ new Map();
|
|
648
864
|
for (const experiment of experiments) {
|
|
@@ -659,7 +875,7 @@ var writeCoreResearchStageIndex = async (rootDir) => {
|
|
|
659
875
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
660
876
|
families: [...grouped.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([family, experiments2]) => ({ family, experiments: experiments2 }))
|
|
661
877
|
};
|
|
662
|
-
await writeJsonAtomic(
|
|
878
|
+
await writeJsonAtomic(import_node_path2.default.join(resolvedRoot, "index.json"), index);
|
|
663
879
|
return index;
|
|
664
880
|
};
|
|
665
881
|
|
|
@@ -676,8 +892,8 @@ var closeOutput = async (output) => {
|
|
|
676
892
|
await (0, import_node_events.once)(output, "close");
|
|
677
893
|
};
|
|
678
894
|
var writeCoreResearchMatches = async (params) => {
|
|
679
|
-
await import_promises2.default.mkdir(
|
|
680
|
-
const output = (0,
|
|
895
|
+
await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
|
|
896
|
+
const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
|
|
681
897
|
await writeLine(
|
|
682
898
|
output,
|
|
683
899
|
[
|
|
@@ -727,8 +943,8 @@ var writeCoreResearchMatches = async (params) => {
|
|
|
727
943
|
await closeOutput(output);
|
|
728
944
|
};
|
|
729
945
|
var writeCoreResearchTrades = async (params) => {
|
|
730
|
-
await import_promises2.default.mkdir(
|
|
731
|
-
const output = (0,
|
|
946
|
+
await import_promises2.default.mkdir(import_node_path3.default.dirname(params.filePath), { recursive: true });
|
|
947
|
+
const output = (0, import_node_fs3.createWriteStream)(params.filePath, { encoding: "utf8" });
|
|
732
948
|
for (const variant of params.variants) {
|
|
733
949
|
for (const trade of variant.trades) {
|
|
734
950
|
await writeLine(
|
|
@@ -1461,9 +1677,9 @@ var compareCoreResearchVariants = (params) => {
|
|
|
1461
1677
|
|
|
1462
1678
|
// src/lib/coreResearch/dataset.ts
|
|
1463
1679
|
var import_node_crypto3 = require("crypto");
|
|
1464
|
-
var
|
|
1465
|
-
var
|
|
1466
|
-
var
|
|
1680
|
+
var import_node_fs4 = require("fs");
|
|
1681
|
+
var import_node_path4 = __toESM(require("path"));
|
|
1682
|
+
var import_node_readline2 = __toESM(require("readline"));
|
|
1467
1683
|
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1468
1684
|
var finiteNumber = (value, field) => {
|
|
1469
1685
|
const numeric = Number(value);
|
|
@@ -1673,11 +1889,11 @@ var readCoreResearchVariant = async (variant) => {
|
|
|
1673
1889
|
const seen = /* @__PURE__ */ new Map();
|
|
1674
1890
|
let duplicateRowsDropped = 0;
|
|
1675
1891
|
for (const inputPath of variant.files) {
|
|
1676
|
-
const filePath =
|
|
1677
|
-
const input = (0,
|
|
1892
|
+
const filePath = import_node_path4.default.resolve(inputPath);
|
|
1893
|
+
const input = (0, import_node_fs4.createReadStream)(filePath);
|
|
1678
1894
|
const hash = (0, import_node_crypto3.createHash)("sha256");
|
|
1679
1895
|
input.on("data", (chunk) => hash.update(chunk));
|
|
1680
|
-
const lines =
|
|
1896
|
+
const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
|
|
1681
1897
|
const fileTrades = [];
|
|
1682
1898
|
let lineNumber = 0;
|
|
1683
1899
|
let rows = 0;
|
|
@@ -2165,79 +2381,6 @@ var applyCoreResearchRobustnessGuardrails = (params) => {
|
|
|
2165
2381
|
}
|
|
2166
2382
|
};
|
|
2167
2383
|
|
|
2168
|
-
// src/lib/coreResearch/trace.ts
|
|
2169
|
-
var import_node_fs4 = require("fs");
|
|
2170
|
-
var import_node_readline2 = __toESM(require("readline"));
|
|
2171
|
-
var import_node_path4 = __toESM(require("path"));
|
|
2172
|
-
var summarizeCoreResearchTrace = async (filePaths = []) => {
|
|
2173
|
-
const events = /* @__PURE__ */ Object.create(null);
|
|
2174
|
-
const skipCounts = /* @__PURE__ */ Object.create(null);
|
|
2175
|
-
const exitCodes = /* @__PURE__ */ Object.create(null);
|
|
2176
|
-
const policyRejections = {};
|
|
2177
|
-
const policyExamples = [];
|
|
2178
|
-
for (const inputPath of filePaths) {
|
|
2179
|
-
const filePath = import_node_path4.default.resolve(inputPath);
|
|
2180
|
-
const input = (0, import_node_fs4.createReadStream)(filePath);
|
|
2181
|
-
const lines = import_node_readline2.default.createInterface({ input, crlfDelay: Infinity });
|
|
2182
|
-
let index = -1;
|
|
2183
|
-
try {
|
|
2184
|
-
for await (const line of lines) {
|
|
2185
|
-
index++;
|
|
2186
|
-
if (!line.trim()) continue;
|
|
2187
|
-
let event;
|
|
2188
|
-
try {
|
|
2189
|
-
event = JSON.parse(line);
|
|
2190
|
-
} catch (error) {
|
|
2191
|
-
throw new Error(
|
|
2192
|
-
`${filePath}:${index + 1} contains invalid trace JSON: ${String(error)}`
|
|
2193
|
-
);
|
|
2194
|
-
}
|
|
2195
|
-
events[event.event] = (events[event.event] ?? 0) + 1;
|
|
2196
|
-
if (event.event === "skip_summary") {
|
|
2197
|
-
for (const [reason, count] of Object.entries(event.skipCounts)) {
|
|
2198
|
-
skipCounts[reason] = (skipCounts[reason] ?? 0) + count;
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
if (event.event === "position_exited" && event.exitCode) {
|
|
2202
|
-
exitCodes[event.exitCode] = (exitCodes[event.exitCode] ?? 0) + 1;
|
|
2203
|
-
}
|
|
2204
|
-
if ("researchEntryDecision" in event && event.researchEntryDecision && !event.researchEntryDecision.allowed) {
|
|
2205
|
-
const reason = event.researchEntryDecision.reason;
|
|
2206
|
-
policyRejections[reason] = (policyRejections[reason] ?? 0) + 1;
|
|
2207
|
-
if (policyExamples.length < 20)
|
|
2208
|
-
policyExamples.push({
|
|
2209
|
-
timestamp: event.timestamp,
|
|
2210
|
-
symbol: event.symbol,
|
|
2211
|
-
decision: event.researchEntryDecision
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
} finally {
|
|
2216
|
-
lines.close();
|
|
2217
|
-
input.destroy();
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
return {
|
|
2221
|
-
events: Object.fromEntries(Object.entries(events).sort()),
|
|
2222
|
-
skipCounts: Object.fromEntries(
|
|
2223
|
-
Object.entries(skipCounts).sort(
|
|
2224
|
-
([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
|
|
2225
|
-
)
|
|
2226
|
-
),
|
|
2227
|
-
exitCodes: Object.fromEntries(
|
|
2228
|
-
Object.entries(exitCodes).sort(
|
|
2229
|
-
([leftKey, leftValue], [rightKey, rightValue]) => rightValue - leftValue || (leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0)
|
|
2230
|
-
)
|
|
2231
|
-
),
|
|
2232
|
-
...policyExamples.length ? {
|
|
2233
|
-
researchPolicy: {
|
|
2234
|
-
rejections: policyRejections,
|
|
2235
|
-
examples: policyExamples
|
|
2236
|
-
}
|
|
2237
|
-
} : {}
|
|
2238
|
-
};
|
|
2239
|
-
};
|
|
2240
|
-
|
|
2241
2384
|
// src/lib/researchRoots.ts
|
|
2242
2385
|
var import_node_path5 = __toESM(require("path"));
|
|
2243
2386
|
var SOURCE_REPOSITORY_ROOT_ENV = "TRADEJS_SOURCE_REPOSITORY_ROOT";
|
|
@@ -2261,6 +2404,7 @@ var summarizeSupplementalFiles = async (params) => {
|
|
|
2261
2404
|
var analyzeCoreResearchPrepared = async (spec, options = {}) => {
|
|
2262
2405
|
const prepared = options.prepared ?? await prepareCoreResearch(spec);
|
|
2263
2406
|
const { paths, specSha256 } = prepared;
|
|
2407
|
+
await verifyPreparedSupport(prepared);
|
|
2264
2408
|
if (prepared.manifest.status === "completed") {
|
|
2265
2409
|
throw new Error(
|
|
2266
2410
|
`Research ${spec.researchId} is already completed and immutable; use verify or create a new researchId`
|
|
@@ -2531,6 +2675,7 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
|
|
|
2531
2675
|
"utf8"
|
|
2532
2676
|
);
|
|
2533
2677
|
const artifactHashes = {
|
|
2678
|
+
...prepared.manifest.artifactHashes,
|
|
2534
2679
|
"spec.json": await sha256File(paths.specPath),
|
|
2535
2680
|
"result.json": await sha256File(paths.resultPath),
|
|
2536
2681
|
"comparison.json": await sha256File(paths.comparisonPath),
|
|
@@ -2821,6 +2966,7 @@ var runCoreResearch = async (params) => {
|
|
|
2821
2966
|
const { spec } = params;
|
|
2822
2967
|
const cwd = import_node_path6.default.resolve(params.cwd ?? process.cwd());
|
|
2823
2968
|
const prepared = await prepareCoreResearch(spec);
|
|
2969
|
+
await verifyPreparedSupport(prepared);
|
|
2824
2970
|
const resolvedVariants = [];
|
|
2825
2971
|
for (const variant of spec.variants) {
|
|
2826
2972
|
if (!variant.command?.length) {
|
|
@@ -174,7 +174,7 @@ function normalizeTrade(raw, fallbackStrategy, candlesBySymbol = {}) {
|
|
|
174
174
|
pockets,
|
|
175
175
|
features,
|
|
176
176
|
groups,
|
|
177
|
-
candles:
|
|
177
|
+
candles: r.candles === void 0 ? candlesBySymbol[symbol] ?? [] : normalizeCandles(r.candles),
|
|
178
178
|
figures: normalizeFigures(r.figures ?? payload.figures)
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -196,11 +196,16 @@ function normalizeDataset(input, options = {}) {
|
|
|
196
196
|
);
|
|
197
197
|
if (runs.size > 1)
|
|
198
198
|
throw new Error("Mixed backtest runs; provide one immutable lineage");
|
|
199
|
+
const candlesBySymbol = Object.fromEntries(
|
|
200
|
+
Object.entries(options.candlesBySymbol ?? object(root.candlesBySymbol)).map(
|
|
201
|
+
([symbol, candles]) => [symbol, normalizeCandles(candles)]
|
|
202
|
+
)
|
|
203
|
+
);
|
|
199
204
|
const trades = rows.map(
|
|
200
205
|
(r) => normalizeTrade(
|
|
201
206
|
r,
|
|
202
207
|
options.strategy ?? String(root.strategy ?? ""),
|
|
203
|
-
|
|
208
|
+
candlesBySymbol
|
|
204
209
|
)
|
|
205
210
|
);
|
|
206
211
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -217,6 +222,21 @@ function normalizeDataset(input, options = {}) {
|
|
|
217
222
|
trades
|
|
218
223
|
};
|
|
219
224
|
}
|
|
225
|
+
function tradeCandleWindow(trade, intervalMs) {
|
|
226
|
+
const lowerBound = (timestamp) => {
|
|
227
|
+
let lo = 0, hi = trade.candles.length;
|
|
228
|
+
while (lo < hi) {
|
|
229
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
230
|
+
if (trade.candles[mid].timestamp < timestamp) lo = mid + 1;
|
|
231
|
+
else hi = mid;
|
|
232
|
+
}
|
|
233
|
+
return lo;
|
|
234
|
+
};
|
|
235
|
+
return trade.candles.slice(
|
|
236
|
+
lowerBound(trade.signalTime - 140 * intervalMs),
|
|
237
|
+
lowerBound(trade.exitTime + 20 * intervalMs + 1)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
220
240
|
|
|
221
241
|
// src/lib/researchVisuals/analysis.ts
|
|
222
242
|
var import_node_crypto = require("crypto");
|
|
@@ -538,7 +558,10 @@ function pricePanel(t, c, step, x, y, w, h) {
|
|
|
538
558
|
);
|
|
539
559
|
}
|
|
540
560
|
function renderTradeSvg(t, intervalMs) {
|
|
541
|
-
const candles = chartCandles(
|
|
561
|
+
const candles = chartCandles({
|
|
562
|
+
...t,
|
|
563
|
+
candles: tradeCandleWindow(t, intervalMs)
|
|
564
|
+
});
|
|
542
565
|
const start = Math.min(
|
|
543
566
|
t.signalTime - 90 * intervalMs,
|
|
544
567
|
t.entryTime - 90 * intervalMs
|
|
@@ -813,9 +836,7 @@ async function generateResearchVisuals(options) {
|
|
|
813
836
|
population: "selected",
|
|
814
837
|
trades: selected.map((trade) => ({
|
|
815
838
|
...trade,
|
|
816
|
-
candles: trade.
|
|
817
|
-
(bar) => bar.timestamp >= trade.signalTime - 140 * dataset.intervalMs && bar.timestamp <= trade.exitTime + 20 * dataset.intervalMs
|
|
818
|
-
)
|
|
839
|
+
candles: tradeCandleWindow(trade, dataset.intervalMs)
|
|
819
840
|
}))
|
|
820
841
|
};
|
|
821
842
|
await write("chart-inputs.json.gz", (0, import_node_zlib.gzipSync)(JSON.stringify(retained)));
|