Skip to content

Commit e5d9dd2

Browse files
committed
feat: add Regime Trend v1 exit timing runner
1 parent d5f2ba8 commit e5d9dd2

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { readFile, mkdir, writeFile } from "node:fs/promises";
2+
import { dirname, join } from "node:path";
3+
import { fileURLToPath, pathToFileURL } from "node:url";
4+
import { sha256 } from "./dataset-tools.mjs";
5+
import {
6+
RESEARCH_PARTITIONS,
7+
filterPartition,
8+
parseCsvCandles,
9+
splitContiguousCandles
10+
} from "./backtest-tools.mjs";
11+
import {
12+
EXIT_TIMING_THRESHOLDS_ATR,
13+
EXIT_TIMING_WINDOWS_BARS,
14+
analyzeExitTiming,
15+
summarizeExitTimingRows
16+
} from "./exit-timing-tools.mjs";
17+
18+
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
19+
const dataDirectory = join(scriptDirectory, "data");
20+
const outputDirectory = join(scriptDirectory, "results");
21+
const HOLDOUT_START_MS = Date.parse("2025-01-01T00:00:00.000Z");
22+
23+
function groupRows(rows, keyFn) {
24+
const groups = new Map();
25+
for (const row of rows) {
26+
const key = keyFn(row);
27+
if (!groups.has(key)) groups.set(key, []);
28+
groups.get(key).push(row);
29+
}
30+
return Object.fromEntries(
31+
[...groups.entries()].map(([key, group]) => [key, summarizeExitTimingRows(group)])
32+
);
33+
}
34+
35+
async function main() {
36+
const { runRegimeTrendV1 } = await import(
37+
pathToFileURL(join(scriptDirectory, "reference-engine.ts")).href
38+
);
39+
const manifest = JSON.parse(await readFile(join(dataDirectory, "dataset-manifest.json"), "utf8"));
40+
await mkdir(outputDirectory, { recursive: true });
41+
42+
const report = {
43+
schema_version: 1,
44+
strategy_id: "regime-trend-v1",
45+
generated_at: new Date().toISOString(),
46+
holdout_opened: false,
47+
holdout_start: new Date(HOLDOUT_START_MS).toISOString(),
48+
thresholds_atr: EXIT_TIMING_THRESHOLDS_ATR,
49+
timing_windows_bars: EXIT_TIMING_WINDOWS_BARS,
50+
partitions: []
51+
};
52+
53+
for (const partition of RESEARCH_PARTITIONS) {
54+
if (partition.endExclusive > HOLDOUT_START_MS) {
55+
throw new Error(`Partition ${partition.id} would open final holdout`);
56+
}
57+
58+
const rows = [];
59+
for (const file of manifest.files) {
60+
const csv = await readFile(join(dataDirectory, file.file), "utf8");
61+
if (sha256(csv) !== file.sha256) throw new Error(`SHA-256 mismatch for ${file.symbol}`);
62+
const candles = filterPartition(
63+
parseCsvCandles(csv, { endExclusive: HOLDOUT_START_MS }),
64+
partition
65+
);
66+
67+
for (const segment of splitContiguousCandles(candles)) {
68+
if (segment.length < 201) continue;
69+
const result = runRegimeTrendV1(segment, {
70+
datasetHash: file.sha256,
71+
symbol: file.symbol,
72+
implementationVersion: "typescript-reference-v1.1.0-exit-timing"
73+
});
74+
for (const trade of result.trades) {
75+
rows.push({
76+
symbol: file.symbol,
77+
exit_reason: trade.exit_reason,
78+
winning_trade: trade.net_pnl > 0,
79+
trade,
80+
timing: analyzeExitTiming(trade, segment)
81+
});
82+
}
83+
}
84+
}
85+
86+
const winners = rows.filter((row) => row.winning_trade);
87+
const losers = rows.filter((row) => !row.winning_trade);
88+
const partitionReport = {
89+
id: partition.id,
90+
start: new Date(partition.start).toISOString(),
91+
end_exclusive: new Date(partition.endExclusive).toISOString(),
92+
all: summarizeExitTimingRows(rows),
93+
winners: summarizeExitTimingRows(winners),
94+
losers: summarizeExitTimingRows(losers),
95+
by_symbol: groupRows(rows, (row) => row.symbol),
96+
by_exit_reason: groupRows(rows, (row) => row.exit_reason),
97+
rows
98+
};
99+
report.partitions.push(partitionReport);
100+
}
101+
102+
const reportPath = join(outputDirectory, "exit-timing-report.json");
103+
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
104+
105+
for (const partition of report.partitions) {
106+
console.log(`${partition.id}: trades=${partition.all.trades}`);
107+
for (const threshold of EXIT_TIMING_THRESHOLDS_ATR) {
108+
const item = partition.losers.thresholds[String(threshold)];
109+
console.log(
110+
` losers ${threshold.toFixed(2)} ATR: ` +
111+
`high-touch=${item.high_touch.reached_count}/${partition.losers.trades}, ` +
112+
`close-confirm=${item.close_confirmation.reached_count}/${partition.losers.trades}, ` +
113+
`median-high-bars=${item.high_touch.median_bars_to_reach ?? "n/a"}`
114+
);
115+
}
116+
}
117+
console.log(`Report written to ${reportPath}`);
118+
console.log("Final holdout was not opened.");
119+
}
120+
121+
main().catch((error) => {
122+
console.error(error instanceof Error ? error.stack ?? error.message : error);
123+
process.exitCode = 1;
124+
});

0 commit comments

Comments
 (0)