SPB Git

spb/wp40_uqo

Public MIT

WP40 RAID: paper sections (setup, micro/end-to-end/applications, threats, reproducibility, conclusion, appendices), E1–E3 reruns, parallel realized post-processing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 2, 2026) parent a9731b7 Browse files

32 changed files +1,943 −11

modified backends/metal/host/metal_backend.mm +3 −3
@@ -237,9 +237,9 @@ void regression_stats(std::span<const f64> x, std::span<const f64> y, std::span<
237 }; 237 };
238 238
239 std::shared_ptr<Backend> make_metal_backend(const PrecisionPolicy& policy) { 239 std::shared_ptr<Backend> make_metal_backend(const PrecisionPolicy& policy) {
240− static std::mutex m; static std::weak_ptr<MetalBackend> cached; std::lock_guard<std::mutex> lk(m); 240+ static std::mutex m; static std::shared_ptr<MetalBackend> cached; std::lock_guard<std::mutex> lk(m);
241− if (auto p = cached.lock()) return p; // one device/queue/library per process (pipelines and buffers are reused) 241+ if (cached) return cached->available() ? cached : nullptr; // one device/queue/library/pipeline cache/buffer pool per process
242− auto p = std::make_shared<MetalBackend>(policy); cached = p; if (!p->available()) return nullptr; return p; 242+ cached = std::make_shared<MetalBackend>(policy); return cached->available() ? cached : nullptr;
243 } 243 }
244 244
245 /// Fused realized accumulators when the backend is Metal (falls back to the four generic calls otherwise). 245 /// Fused realized accumulators when the backend is Metal (falls back to the four generic calls otherwise).
modified benchmarks/end_to_end/bench_e3_garch.cpp +1 −1
@@ -44,7 +44,7 @@ int main(int argc, char** argv) {
44 ThreadPool::global().parallel_for(S, [&](usize a, usize b) { for (usize i = a; i < b; ++i) { auto& r = ret[syms[i]]; std::vector<f64> train(r.begin(), r.end() - static_cast<long>(test)); models::GarchFitOptions o; o.model = sp.m; o.dist = sp.d; o.forecast_horizon = 1; json row = {{"symbol", syms[i]}, {"model", sp.name}, {"n_train", train.size()}}; 44 ThreadPool::global().parallel_for(S, [&](usize a, usize b) { for (usize i = a; i < b; ++i) { auto& r = ret[syms[i]]; std::vector<f64> train(r.begin(), r.end() - static_cast<long>(test)); models::GarchFitOptions o; o.model = sp.m; o.dist = sp.d; o.forecast_horizon = 1; json row = {{"symbol", syms[i]}, {"model", sp.name}, {"n_train", train.size()}};
45 try { const f64 f0 = now(); auto e = models::garch_fit(train, o); row["seconds"] = now() - f0; row["converged"] = e.ok(); row["iterations"] = e.convergence.iterations; row["fevals"] = e.convergence.function_evaluations; row["loglik"] = -e.loss; row["coef"] = e.coefficients; row["se"] = e.inference ? e.inference->std_errors : std::vector<f64>{}; row["persistence"] = e.extra.value("persistence", 0.0); row["warnings"] = e.warnings; 45 try { const f64 f0 = now(); auto e = models::garch_fit(train, o); row["seconds"] = now() - f0; row["converged"] = e.ok(); row["iterations"] = e.convergence.iterations; row["fevals"] = e.convergence.function_evaluations; row["loglik"] = -e.loss; row["coef"] = e.coefficients; row["se"] = e.inference ? e.inference->std_errors : std::vector<f64>{}; row["persistence"] = e.extra.value("persistence", 0.0); row["warnings"] = e.warnings;
46 // rolling 1-step forecasts over the test window WITHOUT refitting (parameters fixed at the training estimate; variance path updated with realized returns): QLIKE vs next-day RV 46 // rolling 1-step forecasts over the test window WITHOUT refitting (parameters fixed at the training estimate; variance path updated with realized returns): QLIKE vs next-day RV
47− if (e.ok()) { std::vector<f64> fc, act; for (usize k = 0; k < test; ++k) { std::vector<f64> hist(r.begin(), r.end() - static_cast<long>(test - k)); auto f = models::garch_forecast(e, hist, 1); fc.push_back(f[0]); act.push_back(rvd[syms[i]][r.size() - test + k]); } auto l = est::forecast_loss(act, fc); row["qlike_vs_rv"] = l.qlike; row["mse_vs_rv"] = l.mse; row["n_forecasts"] = l.n; std::vector<f64> naive; for (usize k = 0; k < test; ++k) naive.push_back(rvd[syms[i]][r.size() - test + k - 1]); auto ln = est::forecast_loss(act, naive); row["qlike_rw_rv"] = ln.qlike; } } 47+ if (e.ok()) { std::vector<f64> fc, act; for (usize k = 0; k < test; ++k) { std::vector<f64> hist(r.begin(), r.end() - static_cast<long>(test - k)); auto f = models::garch_forecast(e, hist, 1); fc.push_back(f[0]); act.push_back(rvd[syms[i]][rvd[syms[i]].size() - test + k]); } auto l = est::forecast_loss(act, fc); row["qlike_vs_rv"] = l.qlike; row["mse_vs_rv"] = l.mse; row["n_forecasts"] = l.n; std::vector<f64> naive; for (usize k = 0; k < test; ++k) naive.push_back(rvd[syms[i]][rvd[syms[i]].size() - test + k - 1]); auto ln = est::forecast_loss(act, naive); row["qlike_rw_rv"] = ln.qlike; } }
48 catch (const RaidError& ex) { row["error"] = ex.what(); row["converged"] = false; } rows[i] = row; } }); 48 catch (const RaidError& ex) { row["error"] = ex.what(); row["converged"] = false; } rows[i] = row; } });
49 usize conv = 0; for (auto& r : rows) { conv += r.value("converged", false); r["kind"] = "fit"; r["experiment"] = "E3"; out << r.dump() << "\n"; } 49 usize conv = 0; for (auto& r : rows) { conv += r.value("converged", false); r["kind"] = "fit"; r["experiment"] = "E3"; out << r.dump() << "\n"; }
50 out << json({{"kind", "fit_summary"}, {"experiment", "E3"}, {"model", sp.name}, {"S", S}, {"converged", conv}, {"wall_seconds_all", now() - t0}, {"threads", ThreadPool::global().size()}}).dump() << "\n"; out.flush(); 50 out << json({{"kind", "fit_summary"}, {"experiment", "E3"}, {"model", sp.name}, {"S", S}, {"converged", conv}, {"wall_seconds_all", now() - t0}, {"threads", ThreadPool::global().size()}}).dump() << "\n"; out.flush();
added experiments/manifests/paper_assets.json +28 −0
@@ -0,0 +1,28 @@
1+{
2+ "inputs": {
3+ "bench_primitives.jsonl": "f3de95928ef0d92f2fb1a89f81ed33118ff01f66ae3b2feb25f6e37d8ace292a",
4+ "e1_batched_rv.jsonl": "853960fae2a0aebe19ee97df2a255d044e105ce6911c7f5683fbdf871e0b7f12",
5+ "e2_har.jsonl": "b9ae608a015b33bea38aa9dfc8a282e9544f70a584d144e7160497c379da75a5",
6+ "e3_garch.jsonl": "679ac18132f6b16ce5618dfe15e9c5c6f416cf40d90f3515d893deb7a4131f91",
7+ "e4_connectedness.jsonl": "27f92310d6950ed7a60e5e9bfe1e361b7ac7f7a8636816475a2f02da8bad7673",
8+ "e5_inference.jsonl": "d4dcc2baa0a227334ac454d928a4ab8c27e209edbad6a4681f5d50a8a0ddff5b",
9+ "e6_pipeline.jsonl": "0816f1510693c4c028fe45b78482d7a8b009c6c9b4929f9a149d079401f935b0",
10+ "e6_baselines.jsonl": "cdbd901b0fa99cd2db9240b5fba13b9f00a852b40d010a9d64f4efcff218cdb8",
11+ "e7_summary.json": "1cbaa525a98b098eb26264d922033ee4d4f2df32049a6cf4b6fcb2a2f80a42cf"
12+ },
13+ "outputs": {
14+ "tables/tab_primitives.tex": "dfccbd67b68adc9c451a2f7f196a70de42cb612ecc9826285e2bc7fadb1db0ff",
15+ "figures/fig_primitives_throughput.pdf": "e9ce50655262712762064350c514e8dfd09fc2fce5c8db2027ed0e74b32b09eb",
16+ "tables/tab_e1.tex": "c5a0de42b79895abd2dc0cffba23b92f9a2048093579fa3529335e24c1a90a97",
17+ "figures/fig_e1.pdf": "53e26cdf3689df17adaebf18f1f95441f2bac33685ffe6e496efe223014a521d",
18+ "tables/tab_e2_accuracy.tex": "a337d1e20c1b1298f83e55984296b06ebfc41834c723e0270236e78022bb6f65",
19+ "tables/tab_e2_runtime.tex": "c43af1813c748886e5dc5c5f26257d3b14a4cc1dab5ea9cee5672e996f739abf",
20+ "tables/tab_e3_fits.tex": "d5d6adfb8f9644524e5deac5a37ce775df6cf32b7df9cbcad3ffb15a1a63587e",
21+ "tables/tab_e4.tex": "645ce562f6fce8f0a07495473237badb1190efe21f99e2dedb444f1b155208ef",
22+ "tables/tab_e5.tex": "69b53b60302201eed7a2ffba95827f88fa44b7ba8b3e642f820ee696652495d7",
23+ "tables/tab_e6.tex": "d06f84a429329af2a4f1e7831cff4cd4371a4189806fea8ba226e73890c47aee",
24+ "tables/tab_e7.tex": "26125ba7860235e8ec9bc7cbf9094525ae3badfa7684b0fdeceff0c444db27bb",
25+ "figures/fig_e7.pdf": "9e32e7a7c8d4fb0e1688ba282d05650b48cc53349a14aee834f4b19d5b306819"
26+ },
27+ "key_numbers_sha256": "4424672bebb9b3b34b9c9465c2afa92a10901aedfd967a1d7dc843855a0db242"
28+}
\ No newline at end of file
added experiments/results/key_numbers.json +1230 −0
@@ -0,0 +1,1230 @@
1+{
2+ "primitives": {
3+ "realized_fused@1x390": {
4+ "cpu1_ms": 0.0014998950064182281,
5+ "cpuMT_ms": 0.0014998950064182281,
6+ "metal_ms": 0.2344171516597271,
7+ "speedup": 0.006398401293585508,
8+ "verdict": "CPU"
9+ },
10+ "realized_fused@1x3900": {
11+ "cpu1_ms": 0.011624768376350403,
12+ "cpuMT_ms": 0.01116609200835228,
13+ "metal_ms": 0.6333747878670692,
14+ "speedup": 0.017629517660396336,
15+ "verdict": "CPU"
16+ },
17+ "realized_fused@10x390": {
18+ "cpu1_ms": 0.011292286217212677,
19+ "cpuMT_ms": 0.14200014993548393,
20+ "metal_ms": 0.2327919937670231,
21+ "speedup": 0.6099872578848089,
22+ "verdict": "CPU"
23+ },
24+ "realized_fused@1x39000": {
25+ "cpu1_ms": 0.18258299678564072,
26+ "cpuMT_ms": 0.1671668142080307,
27+ "metal_ms": 0.206624623388052,
28+ "speedup": 0.8090362681222294,
29+ "verdict": "CPU"
30+ },
31+ "realized_fused@10x3900": {
32+ "cpu1_ms": 0.18791621550917625,
33+ "cpuMT_ms": 0.135000329464674,
34+ "metal_ms": 0.16404176130890846,
35+ "speedup": 0.8229631795433707,
36+ "verdict": "CPU"
37+ },
38+ "realized_fused@100x390": {
39+ "cpu1_ms": 0.1871250569820404,
40+ "cpuMT_ms": 0.5520828999578953,
41+ "metal_ms": 0.22633420303463936,
42+ "speedup": 2.4392376077309077,
43+ "verdict": "Metal"
44+ },
45+ "realized_fused@1x390000": {
46+ "cpu1_ms": 2.032375428825617,
47+ "cpuMT_ms": 2.0858747884631157,
48+ "metal_ms": 0.9353337809443474,
49+ "speedup": 2.2300860195139536,
50+ "verdict": "Metal"
51+ },
52+ "realized_fused@10x39000": {
53+ "cpu1_ms": 2.572625409811735,
54+ "cpuMT_ms": 0.36349985748529434,
55+ "metal_ms": 0.7184999994933605,
56+ "speedup": 0.5059149029110798,
57+ "verdict": "tie"
58+ },
59+ "realized_fused@100x3900": {
60+ "cpu1_ms": 2.043417189270258,
61+ "cpuMT_ms": 0.6015831604599953,
62+ "metal_ms": 0.6413329392671585,
63+ "speedup": 0.9380200573315559,
64+ "verdict": "tie"
65+ },
66+ "realized_fused@1000x390": {
67+ "cpu1_ms": 2.235791180282831,
68+ "cpuMT_ms": 0.8399169892072678,
69+ "metal_ms": 0.33845799043774605,
70+ "speedup": 2.4815989367571367,
71+ "verdict": "Metal"
72+ },
73+ "realized_fused@10x390000": {
74+ "cpu1_ms": 20.89220890775323,
75+ "cpuMT_ms": 2.5267917662858963,
76+ "metal_ms": 1.0219174437224865,
77+ "speedup": 2.4725987229278337,
78+ "verdict": "Metal"
79+ },
80+ "realized_fused@100x39000": {
81+ "cpu1_ms": 20.590624772012234,
82+ "cpuMT_ms": 2.1904585883021355,
83+ "metal_ms": 1.1692089028656483,
84+ "speedup": 1.8734535658542084,
85+ "verdict": "Metal"
86+ },
87+ "realized_fused@1000x3900": {
88+ "cpu1_ms": 21.244540810585022,
89+ "cpuMT_ms": 1.7422502860426903,
90+ "metal_ms": 0.8750832639634609,
91+ "speedup": 1.9909537272506195,
92+ "verdict": "Metal"
93+ },
94+ "realized_fused@10000x390": {
95+ "cpu1_ms": 21.238334011286497,
96+ "cpuMT_ms": 2.125917002558708,
97+ "metal_ms": 2.2132499143481255,
98+ "speedup": 0.9605408719443508,
99+ "verdict": "tie"
100+ },
101+ "realized_fused@100x390000": {
102+ "cpu1_ms": 232.5868341140449,
103+ "cpuMT_ms": 18.79258267581463,
104+ "metal_ms": 8.720249868929386,
105+ "speedup": 2.1550509398559075,
106+ "verdict": "Metal"
107+ },
108+ "realized_fused@1000x39000": {
109+ "cpu1_ms": 238.77166723832488,
110+ "cpuMT_ms": 20.24729223921895,
111+ "metal_ms": 5.891874898225069,
112+ "speedup": 3.4364769430726474,
113+ "verdict": "Metal"
114+ },
115+ "realized_fused@10000x3900": {
116+ "cpu1_ms": 233.6724577471614,
117+ "cpuMT_ms": 15.69391693919897,
118+ "metal_ms": 7.8027499839663506,
119+ "speedup": 2.0113315140749037,
120+ "verdict": "Metal"
121+ },
122+ "regression_stats@1x390": {
123+ "cpu1_ms": 0.0031669624149799347,
124+ "cpuMT_ms": 0.0032088719308376312,
125+ "metal_ms": 0.894167460501194,
126+ "speedup": 0.003588669989219929,
127+ "verdict": "CPU"
128+ },
129+ "regression_stats@1x3900": {
130+ "cpu1_ms": 0.029958784580230713,
131+ "cpuMT_ms": 0.030207913368940353,
132+ "metal_ms": 0.3482499159872532,
133+ "speedup": 0.0867420550075482,
134+ "verdict": "CPU"
135+ },
136+ "regression_stats@10x390": {
137+ "cpu1_ms": 0.031250063329935074,
138+ "cpuMT_ms": 0.04133302718400955,
139+ "metal_ms": 0.6523327901959419,
140+ "speedup": 0.06336187265949686,
141+ "verdict": "CPU"
142+ },
143+ "regression_stats@1x39000": {
144+ "cpu1_ms": 0.30233291909098625,
145+ "cpuMT_ms": 0.3838753327727318,
146+ "metal_ms": 0.4085828550159931,
147+ "speedup": 0.9395287346496851,
148+ "verdict": "tie"
149+ },
150+ "regression_stats@10x3900": {
151+ "cpu1_ms": 0.2997498959302902,
152+ "cpuMT_ms": 0.07625017315149307,
153+ "metal_ms": 0.36174990236759186,
154+ "speedup": 0.2107814615911396,
155+ "verdict": "CPU"
156+ },
157+ "regression_stats@100x390": {
158+ "cpu1_ms": 0.36137504503130913,
159+ "cpuMT_ms": 0.17625000327825546,
160+ "metal_ms": 0.3363746218383312,
161+ "speedup": 0.5239693836444206,
162+ "verdict": "CPU"
163+ },
164+ "regression_stats@1x390000": {
165+ "cpu1_ms": 2.9733749106526375,
166+ "cpuMT_ms": 3.0428748577833176,
167+ "metal_ms": 0.7901252247393131,
168+ "speedup": 3.851129874745179,
169+ "verdict": "Metal"
170+ },
171+ "regression_stats@10x39000": {
172+ "cpu1_ms": 3.083667252212763,
173+ "cpuMT_ms": 0.5624173209071159,
174+ "metal_ms": 0.6961673498153687,
175+ "speedup": 0.8078766133692931,
176+ "verdict": "CPU"
177+ },
178+ "regression_stats@100x3900": {
179+ "cpu1_ms": 2.9982919804751873,
180+ "cpuMT_ms": 0.4399162717163563,
181+ "metal_ms": 0.5946666933596134,
182+ "speedup": 0.739769482011876,
183+ "verdict": "CPU"
184+ },
185+ "regression_stats@1000x390": {
186+ "cpu1_ms": 3.1211250461637974,
187+ "cpuMT_ms": 0.6971252150833607,
188+ "metal_ms": 0.5814996547996998,
189+ "speedup": 1.1988402904959397,
190+ "verdict": "tie"
191+ },
192+ "regression_stats@10x390000": {
193+ "cpu1_ms": 32.662583980709314,
194+ "cpuMT_ms": 6.654791999608278,
195+ "metal_ms": 4.117042291909456,
196+ "speedup": 1.6164011753500427,
197+ "verdict": "Metal"
198+ },
199+ "regression_stats@100x39000": {
200+ "cpu1_ms": 32.19050029292703,
201+ "cpuMT_ms": 3.1737093813717365,
202+ "metal_ms": 4.4694580137729645,
203+ "speedup": 0.7100881967325159,
204+ "verdict": "tie"
205+ },
206+ "regression_stats@1000x3900": {
207+ "cpu1_ms": 34.39616598188877,
208+ "cpuMT_ms": 3.421790897846222,
209+ "metal_ms": 3.796167206019163,
210+ "speedup": 0.9013804482638873,
211+ "verdict": "tie"
212+ },
213+ "regression_stats@10000x390": {
214+ "cpu1_ms": 30.644499696791172,
215+ "cpuMT_ms": 3.9592497050762177,
216+ "metal_ms": 4.190749954432249,
217+ "speedup": 0.944759231194123,
218+ "verdict": "tie"
219+ },
220+ "regression_stats@100x390000": {
221+ "cpu1_ms": 301.3692502863705,
222+ "cpuMT_ms": 33.760249614715576,
223+ "metal_ms": 30.70899983868003,
224+ "speedup": 1.0993601156685116,
225+ "verdict": "Metal"
226+ },
227+ "regression_stats@1000x39000": {
228+ "cpu1_ms": 324.0363751538098,
229+ "cpuMT_ms": 31.240916810929775,
230+ "metal_ms": 26.743208058178425,
231+ "speedup": 1.1681813469411308,
232+ "verdict": "Metal"
233+ },
234+ "regression_stats@10000x3900": {
235+ "cpu1_ms": 356.9141672924161,
236+ "cpuMT_ms": 37.49012481421232,
237+ "metal_ms": 26.574499905109406,
238+ "speedup": 1.4107556096287706,
239+ "verdict": "Metal"
240+ },
241+ "rolling_sumsq@1x390": {
242+ "cpu1_ms": 0.0009578652679920197,
243+ "cpuMT_ms": 0.0007920898497104645,
244+ "metal_ms": 0.2839169465005398,
245+ "speedup": 0.0027898646399008046,
246+ "verdict": "CPU"
247+ },
248+ "rolling_sumsq@1x3900": {
249+ "cpu1_ms": 0.0074999406933784485,
250+ "cpuMT_ms": 0.0074999406933784485,
251+ "metal_ms": 0.2416251227259636,
252+ "speedup": 0.031039573239594054,
253+ "verdict": "CPU"
254+ },
255+ "rolling_sumsq@10x390": {
256+ "cpu1_ms": 0.0071660615503787994,
257+ "cpuMT_ms": 0.01800013706088066,
258+ "metal_ms": 0.2032076008617878,
259+ "speedup": 0.08858003826895974,
260+ "verdict": "CPU"
261+ },
262+ "rolling_sumsq@1x39000": {
263+ "cpu1_ms": 0.0693327747285366,
264+ "cpuMT_ms": 0.07025012746453285,
265+ "metal_ms": 0.16458332538604736,
266+ "speedup": 0.42683623811679494,
267+ "verdict": "CPU"
268+ },
269+ "rolling_sumsq@10x3900": {
270+ "cpu1_ms": 0.06929086521267891,
271+ "cpuMT_ms": 0.04487484693527222,
272+ "metal_ms": 0.16070762649178505,
273+ "speedup": 0.2792328398774908,
274+ "verdict": "CPU"
275+ },
276+ "rolling_sumsq@100x390": {
277+ "cpu1_ms": 0.06991717964410782,
278+ "cpuMT_ms": 0.17983326688408852,
279+ "metal_ms": 0.2676667645573616,
280+ "speedup": 0.6718550477549103,
281+ "verdict": "tie"
282+ },
283+ "rolling_sumsq@1x390000": {
284+ "cpu1_ms": 0.6914162077009678,
285+ "cpuMT_ms": 0.718916766345501,
286+ "metal_ms": 0.7903752848505974,
287+ "speedup": 0.909589128259996,
288+ "verdict": "tie"
289+ },
290+ "rolling_sumsq@10x39000": {
291+ "cpu1_ms": 0.7095411419868469,
292+ "cpuMT_ms": 0.132707878947258,
293+ "metal_ms": 0.7357923313975334,
294+ "speedup": 0.18036050837224432,
295+ "verdict": "CPU"
296+ },
297+ "rolling_sumsq@100x3900": {
298+ "cpu1_ms": 0.806666910648346,
299+ "cpuMT_ms": 0.20312517881393433,
300+ "metal_ms": 0.6820829585194588,
301+ "speedup": 0.2978012810272249,
302+ "verdict": "CPU"
303+ },
304+ "rolling_sumsq@1000x390": {
305+ "cpu1_ms": 0.7188748568296432,
306+ "cpuMT_ms": 0.2710837870836258,
307+ "metal_ms": 0.9075421839952469,
308+ "speedup": 0.29870103215504695,
309+ "verdict": "CPU"
310+ },
311+ "rolling_sumsq@10x390000": {
312+ "cpu1_ms": 6.913874763995409,
313+ "cpuMT_ms": 1.7658751457929611,
314+ "metal_ms": 2.544207964092493,
315+ "speedup": 0.6940765734230536,
316+ "verdict": "CPU"
317+ },
318+ "rolling_sumsq@100x39000": {
319+ "cpu1_ms": 7.338124793022871,
320+ "cpuMT_ms": 1.0874588042497635,
321+ "metal_ms": 2.226833254098892,
322+ "speedup": 0.4883431672524638,
323+ "verdict": "CPU"
324+ },
325+ "rolling_sumsq@1000x3900": {
326+ "cpu1_ms": 6.95441709831357,
327+ "cpuMT_ms": 1.0273326188325882,
328+ "metal_ms": 2.144791651517153,
329+ "speedup": 0.47898947112456725,
330+ "verdict": "CPU"
331+ },
332+ "rolling_sumsq@10000x390": {
333+ "cpu1_ms": 7.047916762530804,
334+ "cpuMT_ms": 1.07216602191329,
335+ "metal_ms": 2.1627498790621758,
336+ "speedup": 0.49574203299838304,
337+ "verdict": "CPU"
338+ },
339+ "rolling_sumsq@100x390000": {
340+ "cpu1_ms": 69.62170917540789,
341+ "cpuMT_ms": 9.413291700184345,
342+ "metal_ms": 14.852333813905716,
343+ "speedup": 0.6337920907333103,
344+ "verdict": "CPU"
345+ },
346+ "rolling_sumsq@1000x39000": {
347+ "cpu1_ms": 70.26320835575461,
348+ "cpuMT_ms": 8.31229193136096,
349+ "metal_ms": 14.728832989931107,
350+ "speedup": 0.5643550943271196,
351+ "verdict": "CPU"
352+ },
353+ "rolling_sumsq@10000x3900": {
354+ "cpu1_ms": 69.22604190185666,
355+ "cpuMT_ms": 6.54304213821888,
356+ "metal_ms": 14.753291849046946,
357+ "speedup": 0.4434970991671636,
358+ "verdict": "CPU"
359+ },
360+ "garch_nll_P1@1x390": {
361+ "cpu1_ms": 0.0017909333109855652,
362+ "cpuMT_ms": 0.0017499551177024841,
363+ "metal_ms": 0.9180000051856041,
364+ "speedup": 0.0019062691806288963,
365+ "verdict": "CPU"
366+ },
367+ "garch_nll_P1@1x3900": {
368+ "cpu1_ms": 0.016250181943178177,
369+ "cpuMT_ms": 0.01695798709988594,
370+ "metal_ms": 1.348957885056734,
371+ "speedup": 0.012571176081729732,
372+ "verdict": "CPU"
373+ },
374+ "garch_nll_P1@10x390": {
375+ "cpu1_ms": 0.021916814148426056,
376+ "cpuMT_ms": 0.02929195761680603,
377+ "metal_ms": 0.3432091325521469,
378+ "speedup": 0.08534725576498299,
379+ "verdict": "CPU"
380+ },
381+ "garch_nll_P1@10x3900": {
382+ "cpu1_ms": 0.2155420370399952,
383+ "cpuMT_ms": 0.06916606798768044,
384+ "metal_ms": 1.4300826005637646,
385+ "speedup": 0.04836508601699924,
386+ "verdict": "CPU"
387+ },
388+ "garch_nll_P1@100x390": {
389+ "cpu1_ms": 0.1916671171784401,
390+ "cpuMT_ms": 0.16116676852107048,
391+ "metal_ms": 0.6238752976059914,
392+ "speedup": 0.2583317036906555,
393+ "verdict": "CPU"
394+ },
395+ "garch_nll_P1@100x3900": {
396+ "cpu1_ms": 1.6154590994119644,
397+ "cpuMT_ms": 0.20695803686976433,
398+ "metal_ms": 1.6493331640958786,
399+ "speedup": 0.12547982504384633,
400+ "verdict": "CPU"
401+ },
402+ "garch_nll_P1@1000x390": {
403+ "cpu1_ms": 1.6390001401305199,
404+ "cpuMT_ms": 0.2765827812254429,
405+ "metal_ms": 1.1531668715178967,
406+ "speedup": 0.23984627728802252,
407+ "verdict": "CPU"
408+ },
409+ "garch_nll_P1@1000x3900": {
410+ "cpu1_ms": 18.310166895389557,
411+ "cpuMT_ms": 1.4978330582380295,
412+ "metal_ms": 7.2068748995661736,
413+ "speedup": 0.20783391957146272,
414+ "verdict": "CPU"
415+ },
416+ "garch_nll_P1@10000x390": {
417+ "cpu1_ms": 17.741417046636343,
418+ "cpuMT_ms": 2.2433330304920673,
419+ "metal_ms": 6.75466563552618,
420+ "speedup": 0.33211607376881125,
421+ "verdict": "CPU"
422+ },
423+ "garch_nll_P1@10000x3900": {
424+ "cpu1_ms": 162.26391727104783,
425+ "cpuMT_ms": 13.83341709151864,
426+ "metal_ms": 61.06641702353954,
427+ "speedup": 0.22653068193252948,
428+ "verdict": "CPU"
429+ },
430+ "garch_nll_P64@1x390": {
431+ "cpu1_ms": 0.08287513628602028,
432+ "cpuMT_ms": 0.17224997282028198,
433+ "metal_ms": 0.5012080073356628,
434+ "speedup": 0.3436696347608925,
435+ "verdict": "CPU"
436+ },
437+ "garch_nll_P64@1x3900": {
438+ "cpu1_ms": 0.8078329265117645,
439+ "cpuMT_ms": 0.14187488704919815,
440+ "metal_ms": 1.4235423877835274,
441+ "speedup": 0.09966326838366861,
442+ "verdict": "CPU"
443+ },
444+ "garch_nll_P64@10x390": {
445+ "cpu1_ms": 0.8257082663476467,
446+ "cpuMT_ms": 0.2438751980662346,
447+ "metal_ms": 1.0179998353123665,
448+ "speedup": 0.23956310168891443,
449+ "verdict": "CPU"
450+ },
451+ "garch_nll_P64@10x3900": {
452+ "cpu1_ms": 8.083874825388193,
453+ "cpuMT_ms": 0.775500200688839,
454+ "metal_ms": 1.3822498731315136,
455+ "speedup": 0.5610419763916696,
456+ "verdict": "CPU"
457+ },
458+ "garch_nll_P64@100x390": {
459+ "cpu1_ms": 8.346542250365019,
460+ "cpuMT_ms": 1.005750149488449,
461+ "metal_ms": 0.5332920700311661,
462+ "speedup": 1.8859274420294159,
463+ "verdict": "Metal"
464+ },
465+ "garch_nll_P64@100x3900": {
466+ "cpu1_ms": 80.1338329911232,
467+ "cpuMT_ms": 6.173874717205763,
468+ "metal_ms": 2.1704169921576977,
469+ "speedup": 2.84455693975565,
470+ "verdict": "Metal"
471+ },
472+ "garch_nll_P64@1000x390": {
473+ "cpu1_ms": 96.79854195564985,
474+ "cpuMT_ms": 8.48708301782608,
475+ "metal_ms": 2.336665987968445,
476+ "speedup": 3.632133587567199,
477+ "verdict": "Metal"
478+ },
479+ "garch_nll_P64@1000x3900": {
480+ "cpu1_ms": 856.1791670508683,
481+ "cpuMT_ms": 81.50566695258021,
482+ "metal_ms": 10.272707790136337,
483+ "speedup": 7.934195016316967,
484+ "verdict": "Metal"
485+ }
486+ },
487+ "breakeven": {
488+ "realized_fused": {
489+ "smallest_metal_win": 39000,
490+ "largest_cpu_win": 39000,
491+ "max_speedup": 3.4364769430726474,
492+ "min_speedup": 0.006398401293585508
493+ },
494+ "regression_stats": {
495+ "smallest_metal_win": 390000,
496+ "largest_cpu_win": 390000,
497+ "max_speedup": 3.851129874745179,
498+ "min_speedup": 0.003588669989219929
499+ },
500+ "rolling_sumsq": {
501+ "smallest_metal_win": null,
502+ "largest_cpu_win": 39000000,
503+ "max_speedup": 0.909589128259996,
504+ "min_speedup": 0.0027898646399008046
505+ },
506+ "garch_nll_P1": {
507+ "smallest_metal_win": null,
508+ "largest_cpu_win": 39000000,
509+ "max_speedup": 0.33211607376881125,
510+ "min_speedup": 0.0019062691806288963
511+ },
512+ "garch_nll_P64": {
513+ "smallest_metal_win": 39000,
514+ "largest_cpu_win": 39000,
515+ "max_speedup": 7.934195016316967,
516+ "min_speedup": 0.09966326838366861
517+ }
518+ },
519+ "E1": {
520+ "real_S1_day": {
521+ "elements": 88350,
522+ "cpu1_ms": 0.7735420949757099,
523+ "cpuMT_ms": 0.9354581125080585,
524+ "metal_ms": 0.9417501278221607,
525+ "metal_unfused_ms": 2.271167002618313,
526+ "speedup_MT": 0.993318805988747,
527+ "fusion_gain": 2.4116450165719523,
528+ "max_rel_err": 3.216772293576152e-08,
529+ "metal_wins": false,
530+ "cpu_wins": false
531+ },
532+ "real_S1_week": {
533+ "elements": 87732,
534+ "cpu1_ms": 0.8084997534751892,
535+ "cpuMT_ms": 0.9053335525095463,
536+ "metal_ms": 0.7747076451778412,
537+ "metal_unfused_ms": 1.9873343408107758,
538+ "speedup_MT": 1.1686131641333148,
539+ "fusion_gain": 2.5652700772748473,
540+ "max_rel_err": 1.5355137638729577e-08,
541+ "metal_wins": true,
542+ "cpu_wins": false
543+ },
544+ "real_S1_month": {
545+ "elements": 85202,
546+ "cpu1_ms": 0.5944999866187572,
547+ "cpuMT_ms": 0.263875350356102,
548+ "metal_ms": 0.32645929604768753,
549+ "metal_unfused_ms": 1.0339580476284027,
550+ "speedup_MT": 0.8082947967809022,
551+ "fusion_gain": 3.167188253317091,
552+ "max_rel_err": 5.502644725750323e-09,
553+ "metal_wins": false,
554+ "cpu_wins": true
555+ },
556+ "real_S1_year": {
557+ "elements": 88350,
558+ "cpu1_ms": 0.8845417760312557,
559+ "cpuMT_ms": 0.7403329946100712,
560+ "metal_ms": 0.5079996772110462,
561+ "metal_unfused_ms": 1.289083156734705,
562+ "speedup_MT": 1.4573493406030318,
563+ "fusion_gain": 2.537566881561543,
564+ "max_rel_err": 1.1553520565395228e-09,
565+ "metal_wins": true,
566+ "cpu_wins": false
567+ },
568+ "real_S10_day": {
569+ "elements": 660818,
570+ "cpu1_ms": 4.591624718159437,
571+ "cpuMT_ms": 1.333874650299549,
572+ "metal_ms": 1.396208070218563,
573+ "metal_unfused_ms": 1.9492078572511673,
574+ "speedup_MT": 0.9553552072584308,
575+ "fusion_gain": 1.3960726189944148,
576+ "max_rel_err": 1.1707127776091667e-07,
577+ "metal_wins": false,
578+ "cpu_wins": false
579+ },
580+ "real_S10_week": {
581+ "elements": 655478,
582+ "cpu1_ms": 4.3206666596233845,
583+ "cpuMT_ms": 1.0201670229434967,
584+ "metal_ms": 0.8149999193847179,
585+ "metal_unfused_ms": 1.9696662202477455,
586+ "speedup_MT": 1.2517388022733416,
587+ "fusion_gain": 2.416768607455495,
588+ "max_rel_err": 4.3164647273400464e-08,
589+ "metal_wins": false,
590+ "cpu_wins": false
591+ },
592+ "real_S10_month": {
593+ "elements": 634608,
594+ "cpu1_ms": 4.162374883890152,
595+ "cpuMT_ms": 0.9038751013576984,
596+ "metal_ms": 1.0571670718491077,
597+ "metal_unfused_ms": 1.4940421096980572,
598+ "speedup_MT": 0.8549974033685291,
599+ "fusion_gain": 1.413250705098868,
600+ "max_rel_err": 1.5374929954074983e-08,
601+ "metal_wins": false,
602+ "cpu_wins": false
603+ },
604+ "real_S10_year": {
605+ "elements": 660818,
606+ "cpu1_ms": 4.490583203732967,
607+ "cpuMT_ms": 1.3055838644504547,
608+ "metal_ms": 1.4134589582681656,
609+ "metal_unfused_ms": 2.377707976847887,
610+ "speedup_MT": 0.9236800664167254,
611+ "fusion_gain": 1.6821910271531078,
612+ "max_rel_err": 3.180737515032068e-09,
613+ "metal_wins": false,
614+ "cpu_wins": false
615+ },
616+ "real_S100_day": {
617+ "elements": 4857139,
618+ "cpu1_ms": 29.412207659333944,
619+ "cpuMT_ms": 3.8021672517061234,
620+ "metal_ms": 4.112166818231344,
621+ "metal_unfused_ms": 7.726917043328285,
622+ "speedup_MT": 0.9246140586634681,
623+ "fusion_gain": 1.8790378369551788,
624+ "max_rel_err": 1.463952514147405e-07,
625+ "metal_wins": false,
626+ "cpu_wins": false
627+ },
628+ "real_S100_week": {
629+ "elements": 4814500,
630+ "cpu1_ms": 28.37820863351226,
631+ "cpuMT_ms": 3.745041787624359,
632+ "metal_ms": 3.630833700299263,
633+ "metal_unfused_ms": 6.0623749159276485,
634+ "speedup_MT": 1.031455058741931,
635+ "fusion_gain": 1.6696922570229453,
636+ "max_rel_err": 1.3949921298073233e-07,
637+ "metal_wins": false,
638+ "cpu_wins": false
639+ },
640+ "real_S100_month": {
641+ "elements": 4656568,
642+ "cpu1_ms": 27.86025032401085,
643+ "cpuMT_ms": 3.613583743572235,
644+ "metal_ms": 3.4282919950783253,
645+ "metal_unfused_ms": 5.321166943758726,
646+ "speedup_MT": 1.054047831619919,
647+ "fusion_gain": 1.552133526373431,
648+ "max_rel_err": 6.515494931810648e-08,
649+ "metal_wins": true,
650+ "cpu_wins": false
651+ },
652+ "real_S100_year": {
653+ "elements": 4817403,
654+ "cpu1_ms": 29.63858423754573,
655+ "cpuMT_ms": 3.890958148986101,
656+ "metal_ms": 3.2545411959290504,
657+ "metal_unfused_ms": 5.527874920517206,
658+ "speedup_MT": 1.1955473643575671,
659+ "fusion_gain": 1.6985112763150025,
660+ "max_rel_err": 1.220912079679724e-08,
661+ "metal_wins": true,
662+ "cpu_wins": false
663+ },
664+ "real_S926_day": {
665+ "elements": 40408948,
666+ "cpu1_ms": 240.89466594159603,
667+ "cpuMT_ms": 29.765665996819735,
668+ "metal_ms": 27.769084088504314,
669+ "metal_unfused_ms": 51.85075011104345,
670+ "speedup_MT": 1.0718994512729338,
671+ "fusion_gain": 1.8672113904004608,
672+ "max_rel_err": 1.6198392060671777e-07,
673+ "metal_wins": false,
674+ "cpu_wins": false
675+ },
676+ "real_S926_week": {
677+ "elements": 40062893,
678+ "cpu1_ms": 235.06820807233453,
679+ "cpuMT_ms": 29.190624598413706,
680+ "metal_ms": 24.931750260293484,
681+ "metal_unfused_ms": 44.04070833697915,
682+ "speedup_MT": 1.1708213139332997,
683+ "fusion_gain": 1.7664507255681425,
684+ "max_rel_err": 1.3949921298073233e-07,
685+ "metal_wins": true,
686+ "cpu_wins": false
687+ },
688+ "real_S926_month": {
689+ "elements": 38769255,
690+ "cpu1_ms": 231.19533294811845,
691+ "cpuMT_ms": 28.66779128089547,
692+ "metal_ms": 20.429750438779593,
693+ "metal_unfused_ms": 38.208582904189825,
694+ "speedup_MT": 1.4032374681620434,
695+ "fusion_gain": 1.8702422733301036,
696+ "max_rel_err": 6.515494931810648e-08,
697+ "metal_wins": true,
698+ "cpu_wins": false
699+ },
700+ "real_S926_year": {
701+ "elements": 39917613,
702+ "cpu1_ms": 241.63200007751584,
703+ "cpuMT_ms": 28.705624863505363,
704+ "metal_ms": 20.413792226463556,
705+ "metal_unfused_ms": 39.6507503464818,
706+ "speedup_MT": 1.406187764872645,
707+ "fusion_gain": 1.9423510294711575,
708+ "max_rel_err": 1.4314531656643865e-08,
709+ "metal_wins": true,
710+ "cpu_wins": false
711+ },
712+ "synthetic_S10000_day": {
713+ "elements": 3900000,
714+ "cpu1_ms": 21.742417011409998,
715+ "cpuMT_ms": 2.6805000379681587,
716+ "metal_ms": 3.3537917770445347,
717+ "metal_unfused_ms": null,
718+ "speedup_MT": 0.7992446210629983,
719+ "fusion_gain": null,
720+ "max_rel_err": 4.9499305589440414e-08,
721+ "metal_wins": false,
722+ "cpu_wins": true
723+ },
724+ "synthetic_S10000_week": {
725+ "elements": 19500000,
726+ "cpu1_ms": 106.63599986582994,
727+ "cpuMT_ms": 12.074083089828491,
728+ "metal_ms": 9.116541128605604,
729+ "metal_unfused_ms": null,
730+ "speedup_MT": 1.3244149200339592,
731+ "fusion_gain": null,
732+ "max_rel_err": 3.356929504238224e-08,
733+ "metal_wins": true,
734+ "cpu_wins": false
735+ },
736+ "synthetic_S10000_month": {
737+ "elements": 85800000,
738+ "cpu1_ms": 483.16720919683576,
739+ "cpuMT_ms": 54.07591676339507,
740+ "metal_ms": 35.40699975565076,
741+ "metal_unfused_ms": null,
742+ "speedup_MT": 1.5272662788878308,
743+ "fusion_gain": null,
744+ "max_rel_err": 2.7721528772327113e-08,
745+ "metal_wins": true,
746+ "cpu_wins": false
747+ }
748+ },
749+ "E2": {
750+ "accuracy": {
751+ "h1": {
752+ "n": 100,
753+ "har": 0.19596646955954006,
754+ "loghar": 0.20549043426692812,
755+ "harq": 0.20287854864925106,
756+ "rw": 0.2509836820568328,
757+ "har_beats_rw_pct": 84.0,
758+ "fits_per_symbol": 132.0
759+ },
760+ "h5": {
761+ "n": 100,
762+ "har": 0.14599330475845337,
763+ "loghar": 0.13780663128811915,
764+ "harq": 0.15032513333481387,
765+ "rw": 0.2392329633290453,
766+ "har_beats_rw_pct": 86.0,
767+ "fits_per_symbol": 128.0
768+ },
769+ "h22": {
770+ "n": 100,
771+ "har": 0.13380122970874658,
772+ "loghar": 0.11202433356845258,
773+ "harq": 0.1317067007400809,
774+ "rw": 0.30092923731362164,
775+ "har_beats_rw_pct": 78.0,
776+ "fits_per_symbol": 111.0
777+ }
778+ },
779+ "runtime": {
780+ "10": {
781+ "per_series_cpu1": 0.07679173722863197,
782+ "per_series_cpuMT": 0.053416937589645386,
783+ "batched_cpu": 0.10233419016003609,
784+ "batched_metal": 0.3199586644768715,
785+ "T": 252
786+ },
787+ "100": {
788+ "per_series_cpu1": 0.7653338834643364,
789+ "per_series_cpuMT": 0.14466699212789536,
790+ "batched_cpu": 0.6509590893983841,
791+ "batched_metal": 0.7528341375291348,
792+ "T": 252
793+ },
794+ "781": {
795+ "per_series_cpu1": 6.033665966242552,
796+ "per_series_cpuMT": 0.6839591078460217,
797+ "batched_cpu": 3.8614580407738686,
798+ "batched_metal": 4.703125450760126,
799+ "T": 251
800+ }
801+ }
802+ },
803+ "E3": {
804+ "symbols": 888,
805+ "daily_history": 845,
806+ "garch.n": {
807+ "n": 888,
808+ "converged_pct": 100.0,
809+ "median_iter": 14.0,
810+ "median_ms": 2.8645414859056473,
811+ "median_persistence": 0.962240963021934,
812+ "qlike": 0.4032196350815048,
813+ "qlike_rw": 0.27550123596742626,
814+ "n_train": 2458.5,
815+ "n_eval": 888
816+ },
817+ "gjr.n": {
818+ "n": 888,
819+ "converged_pct": 98.42342342342343,
820+ "median_iter": 18.0,
821+ "median_ms": 3.9954171516001225,
822+ "median_persistence": 0.9710634836054904,
823+ "qlike": 0.39931764172261175,
824+ "qlike_rw": 0.2734243088287934,
825+ "n_train": 2458.5,
826+ "n_eval": 874
827+ },
828+ "egarch.n": {
829+ "n": 888,
830+ "converged_pct": 92.7927927927928,
831+ "median_iter": 17.0,
832+ "median_ms": 18.152124946936965,
833+ "median_persistence": 0.9561998303082433,
834+ "qlike": 0.39635306701502293,
835+ "qlike_rw": 0.27241123059255185,
836+ "n_train": 2458.5,
837+ "n_eval": 824
838+ },
839+ "garch.t": {
840+ "n": 888,
841+ "converged_pct": 100.0,
842+ "median_iter": 18.0,
843+ "median_ms": 6.74610398709774,
844+ "median_persistence": 0.9727760170446753,
845+ "qlike": 0.3908846216118549,
846+ "qlike_rw": 0.27550123596742626,
847+ "n_train": 2458.5,
848+ "n_eval": 888
849+ },
850+ "grid": {
851+ "S": 888,
852+ "P": 256,
853+ "cpuMT_ms": 117.02079232782125,
854+ "metal_ms": 22.934832610189915,
855+ "max_rel_err_nll": 0.001696265571243037,
856+ "argmin_agreement": 888,
857+ "argmin_total": 888
858+ }
859+ },
860+ "E4": {
861+ "10": {
862+ "days_ok": 182,
863+ "days_skipped": 70,
864+ "cond_median": 80.51310121584409,
865+ "cond_max": 1114.4148164242238,
866+ "lw": 0.15825066990879552,
867+ "dy_windows": 133,
868+ "dy_total": 37.15801851010236,
869+ "dy_ms": 0.5304999649524689,
870+ "dy_failures": 0
871+ },
872+ "30": {
873+ "days_ok": 129,
874+ "days_skipped": 123,
875+ "cond_median": 421.7546397917854,
876+ "cond_max": 1982.0116043100866,
877+ "lw": 0.26298332837244776,
878+ "dy_windows": 133,
879+ "dy_total": 69.67433187119195,
880+ "dy_ms": 2.2807088680565357,
881+ "dy_failures": 0
882+ },
883+ "60": {
884+ "days_ok": 95,
885+ "days_skipped": 157,
886+ "cond_median": 5918.2160036330415,
887+ "cond_max": 4.004356648938842e+297,
888+ "lw": 0.3264154734521777,
889+ "dy_windows": 133,
890+ "dy_total": 87.50466794156553,
891+ "dy_ms": 8.867499884217978,
892+ "dy_failures": 0
893+ },
894+ "100": {
895+ "days_ok": 88,
896+ "days_skipped": 164,
897+ "cond_median": 1.3073204915206876e+298,
898+ "cond_max": 4.628848885035562e+298,
899+ "lw": 0.3323406895220246,
900+ "dy_windows": 133,
901+ "dy_total": 97.24027926801519,
902+ "dy_ms": 28.755417093634605,
903+ "dy_failures": 0
904+ }
905+ },
906+ "E5": {
907+ "garch_n500": {
908+ "mu": {
909+ "bias": -0.00048301580401779385,
910+ "coverage95": 0.95,
911+ "rmse": 0.03830937699585465
912+ },
913+ "omega": {
914+ "bias": 0.020140386483790382,
915+ "coverage95": 0.954,
916+ "rmse": 0.04874063710909834
917+ },
918+ "alpha": {
919+ "bias": 0.0021013443234158386,
920+ "coverage95": 0.896,
921+ "rmse": 0.030365422987920495
922+ },
923+ "beta": {
924+ "bias": -0.025611816484074457,
925+ "coverage95": 0.916,
926+ "rmse": 0.07085785700063447
927+ },
928+ "converged": 1000,
929+ "R": 1000
930+ },
931+ "garch_n2000": {
932+ "mu": {
933+ "bias": 0.0001115840803594372,
934+ "coverage95": 0.961,
935+ "rmse": 0.018994072598395778
936+ },
937+ "omega": {
938+ "bias": 0.0034273370897326973,
939+ "coverage95": 0.972,
940+ "rmse": 0.009288147787445019
941+ },
942+ "alpha": {
943+ "bias": 0.0007353540065208454,
944+ "coverage95": 0.932,
945+ "rmse": 0.013671869751518313
946+ },
947+ "beta": {
948+ "bias": -0.004814929382042228,
949+ "coverage95": 0.936,
950+ "rmse": 0.019568746480840227
951+ },
952+ "converged": 1000,
953+ "R": 1000
954+ },
955+ "har_h1": [
956+ 0.945,
957+ 0.939,
958+ 0.946,
959+ 0.941
960+ ],
961+ "har_h5": [
962+ 0.909,
963+ 0.936,
964+ 0.931,
965+ 0.9
966+ ],
967+ "repro_bootstrap": {
968+ "bitwise_identical_across_threads": true,
969+ "replicas": 2000,
970+ "threads": [
971+ 1,
972+ 4,
973+ 18
974+ ]
975+ },
976+ "repro_realized": {
977+ "cpu_threads_vs_1_identical": true,
978+ "metal_backend_used": "metal",
979+ "metal_runs_identical": true,
980+ "metal_vs_cpu_max_rel_rv": 3.6599268443269906e-08
981+ }
982+ },
983+ "E6": {
984+ "bars_per_file": 2367760.5,
985+ "read_s": 0.06473202048800886,
986+ "prep_s": 0.02098981267772615,
987+ "est_cpu_s": 0.03066229191608727,
988+ "est_metal_s": 0.03653329168446362,
989+ "RAID cpu": {
990+ "median_s": 0.1182449993211776,
991+ "ci": [
992+ 0.09712429158389568,
993+ 0.12820672919042408
994+ ],
995+ "bars_per_s": 20024191.412684422
996+ },
997+ "RAID metal": {
998+ "median_s": 0.12477712496183813,
999+ "ci": [
1000+ 0.10097172926180065,
1001+ 0.134304832899943
1002+ ],
1003+ "bars_per_s": 18975918.067707974
1004+ },
1005+ "polars": {
1006+ "median_s": 0.16476131248055026,
1007+ "ci": [
1008+ 0.14585839601932094,
1009+ 0.17225766699993983
1010+ ],
1011+ "bars_per_s": 14370852.38247
1012+ },
1013+ "duckdb": {
1014+ "median_s": 0.1637031040736474,
1015+ "ci": [
1016+ 0.15818560402840376,
1017+ 0.16537331248400733
1018+ ],
1019+ "bars_per_s": 14463748.341233544
1020+ },
1021+ "pandas": {
1022+ "median_s": 2.5584507705061696,
1023+ "ci": [
1024+ 2.355445542023517,
1025+ 2.6337175624794327
1026+ ],
1027+ "bars_per_s": 925466.5078161957
1028+ }
1029+ },
1030+ "E7": {
1031+ "m2m8a": {
1032+ "chip": "Apple M2",
1033+ "gpu_cores": 10,
1034+ "cpu_threads": "8",
1035+ "mem_gb": 8,
1036+ "macos": "26.5.1",
1037+ "cold_compile_s": 0.109846416,
1038+ "warm_compile_s": 0.000537584,
1039+ "realized_fused@1000x3900_speedup_MT": 3.990399191000979,
1040+ "regression_stats@1000x3900_speedup_MT": 1.6134322509601442,
1041+ "garch_nll_P64@1000x390_speedup_MT": 6.670422238065105,
1042+ "rolling_sumsq@1000x3900_speedup_MT": 0.6518314749487951,
1043+ "realized_fused@1000x3900_cpuMT_ms": 5.524458014406264,
1044+ "realized_fused@1000x3900_metal_ms": 1.3844374334439635,
1045+ "tests": "pass / pass"
1046+ },
1047+ "M2M32c": {
1048+ "chip": "Apple M2 Max",
1049+ "gpu_cores": 30,
1050+ "cpu_threads": "12",
1051+ "mem_gb": 32,
1052+ "macos": "26.5.1",
1053+ "cold_compile_s": 0.048325125,
1054+ "warm_compile_s": 0.000524916,
1055+ "realized_fused@1000x3900_speedup_MT": 3.6053646941003636,
1056+ "regression_stats@1000x3900_speedup_MT": 1.35476806903387,
1057+ "garch_nll_P64@1000x390_speedup_MT": 7.836957668549106,
1058+ "rolling_sumsq@1000x3900_speedup_MT": 0.5173055981132895,
1059+ "realized_fused@1000x3900_cpuMT_ms": 3.611000021919608,
1060+ "realized_fused@1000x3900_metal_ms": 1.0015630396082997,
1061+ "tests": "pass / pass"
1062+ },
1063+ "M2U64": {
1064+ "chip": "Apple M2 Ultra",
1065+ "gpu_cores": 60,
1066+ "cpu_threads": "24",
1067+ "mem_gb": 64,
1068+ "macos": "26.5",
1069+ "cold_compile_s": 0.049152792,
1070+ "warm_compile_s": 0.000613,
1071+ "realized_fused@1000x3900_speedup_MT": 2.1845461259882226,
1072+ "regression_stats@1000x3900_speedup_MT": 1.0830287027429175,
1073+ "garch_nll_P64@1000x390_speedup_MT": 4.924931422444954,
1074+ "rolling_sumsq@1000x3900_speedup_MT": 0.32161785435813445,
1075+ "realized_fused@1000x3900_cpuMT_ms": 2.279437379911542,
1076+ "realized_fused@1000x3900_metal_ms": 1.0434375144541264,
1077+ "tests": "pass / pass"
1078+ },
1079+ "M3BA24": {
1080+ "chip": "Apple M3",
1081+ "gpu_cores": 10,
1082+ "cpu_threads": "8",
1083+ "mem_gb": 24,
1084+ "macos": "26.5.1",
1085+ "cold_compile_s": 0.043905667,
1086+ "warm_compile_s": 0.000421292,
1087+ "realized_fused@1000x3900_speedup_MT": 2.687471016852494,
1088+ "regression_stats@1000x3900_speedup_MT": 1.6792771365359433,
1089+ "garch_nll_P64@1000x390_speedup_MT": 6.207505979251421,
1090+ "rolling_sumsq@1000x3900_speedup_MT": 0.3554353915980856,
1091+ "realized_fused@1000x3900_cpuMT_ms": 4.603916313499212,
1092+ "realized_fused@1000x3900_metal_ms": 1.7131036147475243,
1093+ "tests": "pass / pass"
1094+ },
1095+ "M3U96b": {
1096+ "chip": "Apple M3 Ultra",
1097+ "gpu_cores": 60,
1098+ "cpu_threads": "28",
1099+ "mem_gb": 96,
1100+ "macos": "26.4",
1101+ "cold_compile_s": 0.047089167,
1102+ "warm_compile_s": 0.000531084,
1103+ "realized_fused@1000x3900_speedup_MT": 1.6817560971796317,
1104+ "regression_stats@1000x3900_speedup_MT": 0.7767583073146588,
1105+ "garch_nll_P64@1000x390_speedup_MT": 3.179140922934574,
1106+ "rolling_sumsq@1000x3900_speedup_MT": 0.24312350847240793,
1107+ "realized_fused@1000x3900_cpuMT_ms": 2.304041408933699,
1108+ "realized_fused@1000x3900_metal_ms": 1.370021142065525,
1109+ "tests": "pass / pass"
1110+ },
1111+ "m4md": {
1112+ "chip": "Apple M4",
1113+ "gpu_cores": 10,
1114+ "cpu_threads": "10",
1115+ "mem_gb": 16,
1116+ "macos": "26.6.2",
1117+ "cold_compile_s": 0.042053541,
1118+ "warm_compile_s": 0.000386541,
1119+ "realized_fused@1000x3900_speedup_MT": 2.548863991558486,
1120+ "regression_stats@1000x3900_speedup_MT": 1.338401514661036,
1121+ "garch_nll_P64@1000x390_speedup_MT": 4.206244849053445,
1122+ "rolling_sumsq@1000x3900_speedup_MT": 0.37666800739402895,
1123+ "realized_fused@1000x3900_cpuMT_ms": 3.558958414942026,
1124+ "realized_fused@1000x3900_metal_ms": 1.3962920056656003,
1125+ "tests": "pass / pass"
1126+ },
1127+ "m4mm": {
1128+ "chip": "Apple M4 Pro",
1129+ "gpu_cores": 16,
1130+ "cpu_threads": "12",
1131+ "mem_gb": 48,
1132+ "macos": "26.5.1",
1133+ "cold_compile_s": 0.038549083,
1134+ "warm_compile_s": 0.000449708,
1135+ "realized_fused@1000x3900_speedup_MT": 1.6260083182113196,
1136+ "regression_stats@1000x3900_speedup_MT": 0.91166043986465,
1137+ "garch_nll_P64@1000x390_speedup_MT": 2.1391683174192333,
1138+ "rolling_sumsq@1000x3900_speedup_MT": 0.23880014056164212,
1139+ "realized_fused@1000x3900_cpuMT_ms": 2.68670788500458,
1140+ "realized_fused@1000x3900_metal_ms": 1.6523334197700024,
1141+ "tests": "pass / pass"
1142+ },
1143+ "M4M64c": {
1144+ "chip": "Apple M4 Max",
1145+ "gpu_cores": 40,
1146+ "cpu_threads": "16",
1147+ "mem_gb": 64,
1148+ "macos": "26.4",
1149+ "cold_compile_s": 0.040168875,
1150+ "warm_compile_s": 0.0004675,
1151+ "realized_fused@1000x3900_speedup_MT": 2.79582771769825,
1152+ "regression_stats@1000x3900_speedup_MT": 1.1902389066688093,
1153+ "garch_nll_P64@1000x390_speedup_MT": 4.8188331984067,
1154+ "rolling_sumsq@1000x3900_speedup_MT": 0.4781437102048132,
1155+ "realized_fused@1000x3900_cpuMT_ms": 2.066000597551465,
1156+ "realized_fused@1000x3900_metal_ms": 0.7389584789052606,
1157+ "tests": "pass / pass"
1158+ },
1159+ "m5ma": {
1160+ "chip": "Apple M5 Pro",
1161+ "gpu_cores": 16,
1162+ "cpu_threads": "15",
1163+ "mem_gb": 24,
1164+ "macos": "27.0",
1165+ "cold_compile_s": 0.043711542,
1166+ "warm_compile_s": 0.000386292,
1167+ "realized_fused@1000x3900_speedup_MT": 1.1284910996519257,
1168+ "regression_stats@1000x3900_speedup_MT": 0.5487957656812417,
1169+ "garch_nll_P64@1000x390_speedup_MT": 1.8287923928070076,
1170+ "rolling_sumsq@1000x3900_speedup_MT": 0.19530880040350074,
1171+ "realized_fused@1000x3900_cpuMT_ms": 2.040499937720597,
1172+ "realized_fused@1000x3900_metal_ms": 1.8081666203215718,
1173+ "tests": "pass / pass"
1174+ },
1175+ "M5M36": {
1176+ "chip": "Apple M5 Max",
1177+ "gpu_cores": 32,
1178+ "cpu_threads": "18",
1179+ "mem_gb": 36,
1180+ "macos": "27.0",
1181+ "cold_compile_s": 0.037059042,
1182+ "warm_compile_s": 0.000456708,
1183+ "realized_fused@1000x3900_speedup_MT": 1.1217974525711423,
1184+ "regression_stats@1000x3900_speedup_MT": 0.6015969625826116,
1185+ "garch_nll_P64@1000x390_speedup_MT": 2.1447918847693943,
1186+ "rolling_sumsq@1000x3900_speedup_MT": 0.22944356711535452,
1187+ "realized_fused@1000x3900_cpuMT_ms": 1.6108546406030655,
1188+ "realized_fused@1000x3900_metal_ms": 1.4359585475176573,
1189+ "tests": "pass / pass"
1190+ },
1191+ "m6ma": {
1192+ "chip": "Apple M6",
1193+ "gpu_cores": 12,
1194+ "cpu_threads": "12",
1195+ "mem_gb": 24,
1196+ "macos": "27.0",
1197+ "cold_compile_s": 0.039953334,
1198+ "warm_compile_s": 0.000306458,
1199+ "realized_fused@1000x3900_speedup_MT": 1.6330551240965197,
1200+ "regression_stats@1000x3900_speedup_MT": 0.7851066834472289,
1201+ "garch_nll_P64@1000x390_speedup_MT": 3.7206830701585027,
1202+ "rolling_sumsq@1000x3900_speedup_MT": 0.34461349554716936,
1203+ "realized_fused@1000x3900_cpuMT_ms": 2.4681249633431435,
1204+ "realized_fused@1000x3900_metal_ms": 1.5113543486222625,
1205+ "tests": "pass / pass"
1206+ }
1207+ },
1208+ "laptop": {
1209+ "chip": "Apple M5 Max",
1210+ "os": "27.0",
1211+ "xcode": "Xcode 27.2 Build version 27B52",
1212+ "clang": "Apple clang version 21.0.0 (clang-2100.4.1.1)",
1213+ "cmake": "cmake version 4.3.2",
1214+ "metal_toolchain": "Apple metal version 32023.922 (metalfe-32023.922.4)",
1215+ "brew": {
1216+ "eigen": "eigen 5.0.1",
1217+ "apache-arrow": "apache-arrow 26.0.0_3",
1218+ "catch2": "catch2 3.16.1",
1219+ "nlohmann-json": "nlohmann-json 3.12.0",
1220+ "openblas": "openblas 0.3.33",
1221+ "libomp": "libomp 22.1.8"
1222+ },
1223+ "load_avg": "{ 3,67 3,00 2,56 }",
1224+ "thermal": "Note: No thermal warning level has been recorded;Note: No performance warning level has been recorded;Note: No CPU power status has been recorded;",
1225+ "ncpu": "18",
1226+ "perf_cores": "6",
1227+ "eff_cores": "12"
1228+ },
1229+ "generated_at": "2026-11-02T21:05:20.158901+00:00"
1230+}
\ No newline at end of file
added experiments/scripts/make_paper_assets.py +166 −0
@@ -0,0 +1,166 @@
1+#!/usr/bin/env python3
2+"""Generates every paper table (paper/tables/*.tex) and figure (paper/figures/*.pdf) from the tracked artifacts under
3+experiments/results/, and writes experiments/results/key_numbers.json (the only source of numbers quoted in the prose)
4+and experiments/manifests/paper_assets.json (SHA-256 of every input and output). No number is typed by hand."""
5+import os, sys, json, glob, hashlib, collections, statistics, csv
6+import numpy as np
7+ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."); RES = os.path.join(ROOT, "experiments", "results"); RAW = os.path.join(RES, "raw")
8+TAB = os.path.join(ROOT, "paper", "tables"); FIG = os.path.join(ROOT, "paper", "figures"); os.makedirs(TAB, exist_ok=True); os.makedirs(FIG, exist_ok=True)
9+import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
10+plt.rcParams.update({"font.size": 8, "axes.titlesize": 9, "legend.fontsize": 7, "figure.dpi": 150})
11+COL = {"cpu1": "#9aa0a6", "cpuN": "#1f4e79", "metal": "#c0392b", "metal_unfused": "#e67e22", "pandas": "#7f8c8d", "polars": "#2980b9", "duckdb": "#27ae60", "raid_cpu": "#1f4e79", "raid_metal": "#c0392b"}
12+rng = np.random.default_rng(20261102); key = {}; inputs = {}; outputs = {}
13+def sha(p): return hashlib.sha256(open(p, "rb").read()).hexdigest()
14+def load(name):
15+ p = os.path.join(RAW, name);
16+ if not os.path.exists(p): return []
17+ inputs[name] = sha(p); return [json.loads(l) for l in open(p) if l.strip()]
18+def ci(x, n=2000): x = np.asarray(x, float); m = [np.median(rng.choice(x, x.size, replace=True)) for _ in range(n)]; return float(np.median(x)), float(np.percentile(m, 2.5)), float(np.percentile(m, 97.5))
19+def med(it):
20+ v = [x for x in it if x is not None and np.isfinite(x)]; return statistics.median(v) if v else None
21+def fmt(x, d=2): return "--" if x is None or (isinstance(x, float) and not np.isfinite(x)) else f"{x:.{d}f}"
22+def write(name, s): p = os.path.join(TAB, name); open(p, "w").write(s); outputs["tables/" + name] = sha(p)
23+def savefig(fig, name): p = os.path.join(FIG, name); fig.savefig(p, bbox_inches="tight"); plt.close(fig); outputs["figures/" + name] = sha(p)
24+
25+# ---------------------------------------------------------------- primitives (laptop)
26+prim = [r for r in load("bench_primitives.jsonl") if r.get("kernel") != "shader_library_load"]
27+g = collections.defaultdict(list)
28+for r in prim: g[(r["kernel"], r["S"], r["n"], r["backend"], r["threads"] == 1)].append(r["wall_seconds"])
29+rows = {k: ci(v) for k, v in g.items()}
30+def spd(k, S, n):
31+ c = rows.get((k, S, n, "cpu", False)); m = rows.get((k, S, n, "metal", False)); c1 = rows.get((k, S, n, "cpu", True))
32+ if not c or not m: return None
33+ verdict = "Metal" if m[2] < c[1] else ("CPU" if c[2] < m[1] else "tie"); return (c1[0] if c1 else None, c[0], m[0], c[0] / m[0], verdict)
34+lines = [r"\begin{tabular}{llrrrrrl}", r"\toprule", r"Kernel & $S\times n$ & elements & CPU 1T (ms) & CPU MT (ms) & Metal (ms) & speedup & verdict \\", r"\midrule"]
35+prim_key = {}
36+for k in ["realized_fused", "regression_stats", "rolling_sumsq", "garch_nll_P1", "garch_nll_P64"]:
37+ for (S, n) in sorted({(r["S"], r["n"]) for r in prim if r["kernel"] == k}, key=lambda t: (t[0] * t[1], t[0])):
38+ s = spd(k, S, n)
39+ if not s: continue
40+ prim_key[f"{k}@{S}x{n}"] = {"cpu1_ms": None if s[0] is None else 1e3 * s[0], "cpuMT_ms": 1e3 * s[1], "metal_ms": 1e3 * s[2], "speedup": s[3], "verdict": s[4]}
41+ lines.append(f"{k.replace('_', r'\_')} & ${S}\\times{n}$ & {S * n:,} & {fmt(None if s[0] is None else 1e3 * s[0], 3)} & {fmt(1e3 * s[1], 3)} & {fmt(1e3 * s[2], 3)} & {fmt(s[3])} & {s[4]} \\\\")
42+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_primitives.tex", "\n".join(lines)); key["primitives"] = prim_key
43+# break-even summary
44+be = {}
45+for k in ["realized_fused", "regression_stats", "rolling_sumsq", "garch_nll_P1", "garch_nll_P64"]:
46+ wins = sorted(v["cpuMT_ms"] and (int(t.split("@")[1].split("x")[0]) * int(t.split("x")[1])) for t, v in prim_key.items() if t.startswith(k + "@") and v["verdict"] == "Metal")
47+ losses = sorted((int(t.split("@")[1].split("x")[0]) * int(t.split("x")[1])) for t, v in prim_key.items() if t.startswith(k + "@") and v["verdict"] == "CPU")
48+ be[k] = {"smallest_metal_win": wins[0] if wins else None, "largest_cpu_win": losses[-1] if losses else None, "max_speedup": max(v["speedup"] for t, v in prim_key.items() if t.startswith(k + "@")), "min_speedup": min(v["speedup"] for t, v in prim_key.items() if t.startswith(k + "@"))}
49+key["breakeven"] = be
50+# figure: throughput
51+kernels = ["realized_fused", "regression_stats", "rolling_sumsq", "garch_nll_P64"]; fig, axes = plt.subplots(1, 4, figsize=(10, 2.6))
52+for ax, k in zip(axes, kernels):
53+ for be_, one, lab in [("cpu", True, "CPU 1 thread"), ("cpu", False, "CPU 18 threads"), ("metal", False, "Metal")]:
54+ pts = sorted([(S * n, S * n / rows[(k, S, n, be_, one)][0]) for (kk, S, n, b, o) in rows if kk == k and b == be_ and o == one])
55+ if pts: ax.plot([p[0] for p in pts], [p[1] for p in pts], "o-", ms=2.5, lw=1, color=COL["cpu1" if one else ("cpuN" if be_ == "cpu" else "metal")], label=lab)
56+ ax.set_xscale("log"); ax.set_yscale("log"); ax.set_title(k.replace("_", " ")); ax.set_xlabel("elements"); ax.grid(alpha=.3)
57+axes[0].set_ylabel("elements / s (median)"); axes[0].legend(); savefig(fig, "fig_primitives_throughput.pdf")
58+
59+# ---------------------------------------------------------------- E1
60+e1 = [r for r in load("e1_batched_rv.jsonl") if r.get("kind") == "bench"]
61+g1 = collections.defaultdict(list)
62+for r in e1: g1[(r["source"], r["S"], r["segment"], r["backend"])].append(r["wall_seconds"])
63+r1 = {k: ci(v) for k, v in g1.items()}; err = collections.defaultdict(list)
64+for r in e1:
65+ if r["backend"].startswith("metal"): err[(r["source"], r["S"], r["segment"])].append(r["max_rel_err_rv_vs_fp64"])
66+lines = [r"\begin{tabular}{llrrrrrrl}", r"\toprule", r"Panel & $S$ & segment & elements & CPU 1T (ms) & CPU MT (ms) & Metal fused (ms) & Metal 4 kernels (ms) & max rel.\ err.\ RV \\", r"\midrule"]
67+e1_key = {}
68+for (src, S, seg) in sorted({(r["source"], r["S"], r["segment"]) for r in e1}, key=lambda t: (t[0] != "real", t[1], ["day", "week", "month", "year"].index(t[2]))):
69+ el = next(r["elements"] for r in e1 if (r["source"], r["S"], r["segment"]) == (src, S, seg))
70+ c1, cn, mt, mu = (r1.get((src, S, seg, b)) for b in ["cpu1", "cpuN", "metal", "metal_unfused"]); mx = max(err[(src, S, seg)]) if err[(src, S, seg)] else None
71+ e1_key[f"{src}_S{S}_{seg}"] = {"elements": el, "cpu1_ms": c1 and 1e3 * c1[0], "cpuMT_ms": cn and 1e3 * cn[0], "metal_ms": mt and 1e3 * mt[0], "metal_unfused_ms": mu and 1e3 * mu[0], "speedup_MT": (cn and mt) and cn[0] / mt[0], "fusion_gain": (mu and mt) and mu[0] / mt[0], "max_rel_err": mx, "metal_wins": bool(cn and mt and mt[2] < cn[1]), "cpu_wins": bool(cn and mt and cn[2] < mt[1])}
72+ lines.append(f"{src} & {S} & {seg} & {el:,} & {fmt(c1 and 1e3 * c1[0], 2)} & {fmt(cn and 1e3 * cn[0], 2)} & {fmt(mt and 1e3 * mt[0], 2)} & {fmt(mu and 1e3 * mu[0], 2)} & {('%.1e' % mx) if mx is not None else '--'} \\\\")
73+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e1.tex", "\n".join(lines)); key["E1"] = e1_key
74+fig, axes = plt.subplots(1, 2, figsize=(7, 2.6))
75+for ax, src in zip(axes, ["real", "synthetic"]):
76+ for b, lab in [("cpu1", "CPU 1T"), ("cpuN", "CPU 18T"), ("metal", "Metal fused"), ("metal_unfused", "Metal 4 kernels")]:
77+ pts = sorted([(e1_key[k]["elements"], e1_key[k]["elements"] / (e1_key[k][{"cpu1": "cpu1_ms", "cpuN": "cpuMT_ms", "metal": "metal_ms", "metal_unfused": "metal_unfused_ms"}[b]] / 1e3)) for k in e1_key if k.startswith(src + "_") and e1_key[k][{"cpu1": "cpu1_ms", "cpuN": "cpuMT_ms", "metal": "metal_ms", "metal_unfused": "metal_unfused_ms"}[b]]])
78+ if pts: ax.plot([p[0] for p in pts], [p[1] for p in pts], "o", ms=3, color=COL[b], label=lab)
79+ ax.set_xscale("log"); ax.set_yscale("log"); ax.set_title(f"E1 — {src} panel"); ax.set_xlabel("elements (returns)"); ax.grid(alpha=.3)
80+axes[0].set_ylabel("returns / s"); axes[0].legend(); savefig(fig, "fig_e1.pdf")
81+
82+# ---------------------------------------------------------------- E2
83+e2 = load("e2_har.jsonl"); acc = [r for r in e2 if r.get("kind") == "accuracy"]; rt = [r for r in e2 if r.get("kind") == "runtime"]
84+lines = [r"\begin{tabular}{lrrrrrr}", r"\toprule", r"$h$ & symbols & HAR QLIKE & log-HAR QLIKE & HARQ QLIKE & RW QLIKE & HAR beats RW (\%) \\", r"\midrule"]; e2_key = {}
85+for h in [1, 5, 22]:
86+ rs = [r for r in acc if r["h"] == h and "qlike" in r.get("har", {})]
87+ medq = lambda m: med(r[m]["qlike"] for r in rs if "qlike" in r.get(m, {}))
88+ beats = 100 * np.mean([r["har"]["qlike"] < r["random_walk"]["qlike"] for r in rs])
89+ e2_key[f"h{h}"] = {"n": len(rs), "har": medq("har"), "loghar": medq("loghar"), "harq": medq("harq"), "rw": medq("random_walk"), "har_beats_rw_pct": beats, "fits_per_symbol": med(r["har"]["fits"] for r in rs)}
90+ lines.append(f"{h} & {len(rs)} & {fmt(medq('har'), 3)} & {fmt(medq('loghar'), 3)} & {fmt(medq('harq'), 3)} & {fmt(medq('random_walk'), 3)} & {fmt(beats, 1)} \\\\")
91+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e2_accuracy.tex", "\n".join(lines))
92+g2 = collections.defaultdict(list)
93+for r in rt: g2[(r["S"], r["mode"])].append(r["wall_seconds"])
94+lines = [r"\begin{tabular}{rrrrrr}", r"\toprule", r"$S$ & $T$ & per-series 1T (ms) & per-series 18T (ms) & batched CPU (ms) & batched Metal (ms) \\", r"\midrule"]; e2_rt = {}
95+for S in sorted({r["S"] for r in rt}):
96+ T = next(r["T_per_series"] for r in rt if r["S"] == S); v = {m: ci(g2[(S, m)])[0] for m in ["per_series_cpu1", "per_series_cpuMT", "batched_cpu", "batched_metal"] if g2[(S, m)]}
97+ e2_rt[str(S)] = {**{k: 1e3 * x for k, x in v.items()}, "T": T}; lines.append(f"{S} & {T} & " + " & ".join(fmt(1e3 * v.get(m), 3) if v.get(m) else "--" for m in ["per_series_cpu1", "per_series_cpuMT", "batched_cpu", "batched_metal"]) + r" \\")
98+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e2_runtime.tex", "\n".join(lines)); key["E2"] = {"accuracy": e2_key, "runtime": e2_rt}
99+
100+# ---------------------------------------------------------------- E3
101+e3 = load("e3_garch.jsonl"); fits = [r for r in e3 if r.get("kind") == "fit"]; grid = [r for r in e3 if r.get("kind") == "grid"]; loadrec = next((r for r in e3 if r.get("kind") == "load"), {})
102+lines = [r"\begin{tabular}{lrrrrrrr}", r"\toprule", r"Model & fits & converged (\%) & median iterations & median time (ms) & median persistence & QLIKE vs RV & QLIKE RW \\", r"\midrule"]; e3_key = {"symbols": loadrec.get("symbols"), "daily_history": loadrec.get("daily_history_replaced")}
103+for m in ["garch.n", "gjr.n", "egarch.n", "garch.t"]:
104+ rs = [r for r in fits if r["model"] == m]; ok = [r for r in rs if r.get("converged")]
105+ e3_key[m] = {"n": len(rs), "converged_pct": 100 * len(ok) / len(rs), "median_iter": med(r["iterations"] for r in ok), "median_ms": 1e3 * med(r["seconds"] for r in ok), "median_persistence": med(r["persistence"] for r in ok), "qlike": med(r.get("qlike_vs_rv") for r in ok), "qlike_rw": med(r.get("qlike_rw_rv") for r in ok), "n_train": med(r["n_train"] for r in rs), "n_eval": sum(1 for r in ok if r.get("qlike_vs_rv") is not None)}
106+ k3 = e3_key[m]; lines.append(f"{m} & {k3['n']} & {fmt(k3['converged_pct'], 1)} & {fmt(k3['median_iter'], 0)} & {fmt(k3['median_ms'], 2)} & {fmt(k3['median_persistence'], 3)} & {fmt(k3['qlike'], 3)} & {fmt(k3['qlike_rw'], 3)} \\\\")
107+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e3_fits.tex", "\n".join(lines))
108+gm = [r for r in grid if r["backend"] == "metal"]; gc = [r for r in grid if r["backend"] == "cpuMT"]
109+if gm and gc: e3_key["grid"] = {"S": gm[0]["S"], "P": gm[0]["P"], "cpuMT_ms": 1e3 * statistics.median(r["wall_seconds"] for r in gc), "metal_ms": 1e3 * statistics.median(r["wall_seconds"] for r in gm), "max_rel_err_nll": max(r["max_rel_err_nll"] for r in gm), "argmin_agreement": gm[0]["argmin_agreement"], "argmin_total": gm[0]["S_total"]}
110+key["E3"] = e3_key
111+
112+# ---------------------------------------------------------------- E4
113+e4 = load("e4_connectedness.jsonl"); lines = [r"\begin{tabular}{rrrrrrrr}", r"\toprule", r"$N$ & days used & cond.\ median & cond.\ max & LW shrinkage & DY windows (120 d) & mean total (\%) & time (ms) \\", r"\midrule"]; e4_key = {}
114+for N in sorted({r["N"] for r in e4}):
115+ rc = next((r for r in e4 if r["kind"] == "rcov" and r["N"] == N), None); dy = [r for r in e4 if r["kind"] == "dy" and r["N"] == N and r.get("window") == 120 and "wall_seconds" in r]
116+ if rc: e4_key[str(N)] = {"days_ok": rc["days_ok"], "days_skipped": rc["days_skipped"], "cond_median": rc["cond_median"], "cond_max": rc["cond_max"], "lw": rc["ledoit_wolf_shrinkage_mean"], "dy_windows": dy[0]["n_windows"] if dy else None, "dy_total": dy[0]["mean_total"] if dy else None, "dy_ms": 1e3 * statistics.median(r["wall_seconds"] for r in dy) if dy else None, "dy_failures": dy[0]["failures"] if dy else None}
117+ k4 = e4_key.get(str(N), {}); lines.append(f"{N} & {k4.get('days_ok', '--')} & {fmt(k4.get('cond_median'), 0)} & {fmt(k4.get('cond_max'), 0)} & {fmt(k4.get('lw'), 2)} & {k4.get('dy_windows', '--')} & {fmt(k4.get('dy_total'), 1)} & {fmt(k4.get('dy_ms'), 1)} \\\\")
118+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e4.tex", "\n".join(lines)); key["E4"] = e4_key
119+
120+# ---------------------------------------------------------------- E5
121+e5 = load("e5_inference.jsonl"); lines = [r"\begin{tabular}{llrrrr}", r"\toprule", r"DGP & parameter & bias & RMSE & coverage 95\% & converged \\", r"\midrule"]; e5_key = {}
122+for r in e5:
123+ if r["kind"] == "garch_mc":
124+ e5_key[f"garch_n{r['n']}"] = {p: r[p] for p in ["mu", "omega", "alpha", "beta"]} | {"converged": r["converged"], "R": r["R"]}
125+ for p in ["mu", "omega", "alpha", "beta"]: lines.append(f"GARCH(1,1) $n={r['n']}$ & $\\{p}$ & {r[p]['bias']:+.4f} & {fmt(r[p]['rmse'], 4)} & {fmt(r[p]['coverage95'], 3)} & {r['converged']}/{r['R']} \\\\")
126+ elif r["kind"] == "har_mc":
127+ e5_key[f"har_h{r['h']}"] = r["coverage95"]; lines.append(f"HAR $h={r['h']}$ (HAC) & $\\beta_0,\\beta_d,\\beta_w,\\beta_m$ & -- & -- & {', '.join(fmt(c, 3) for c in r['coverage95'])} & {r['R']}/{r['R']} \\\\")
128+ elif r["kind"] in ("repro_bootstrap", "repro_realized"): e5_key[r["kind"]] = {k: v for k, v in r.items() if k not in ("kind", "experiment")}
129+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e5.tex", "\n".join(lines)); key["E5"] = e5_key
130+
131+# ---------------------------------------------------------------- E6
132+e6 = load("e6_pipeline.jsonl"); e6b = load("e6_baselines.jsonl"); g6 = collections.defaultdict(list); st6 = collections.defaultdict(list)
133+for r in e6: g6["RAID " + r["backend_used"]].append(r["total_seconds"]); st6["read"].append(r["read_seconds"]); st6["prep"].append(r["prep_seconds"]); st6["est_" + r["backend_used"]].append(r["estimate_seconds"]); st6["rss_" + r["backend_used"]].append(r["peak_rss_after"]); st6["bars"].append(r["bars"])
134+for r in e6b: g6[r["engine"]].append(r["total_seconds"]); st6["rss_" + r["engine"]].append(r["peak_rss"])
135+lines = [r"\begin{tabular}{lrrrl}", r"\toprule", r"Engine & median total (s) & 95\% CI & bars / s & notes \\", r"\midrule"]; e6_key = {"bars_per_file": statistics.median(st6["bars"]), "read_s": statistics.median(st6["read"]), "prep_s": statistics.median(st6["prep"]), "est_cpu_s": statistics.median(st6["est_cpu"]), "est_metal_s": statistics.median(st6["est_metal"]) if st6["est_metal"] else None}
136+notes = {"RAID cpu": "Arrow projection, exact calendar, 5 measures", "RAID metal": "same, fused Metal kernel", "pandas": "approximate early-close rule, groupby.apply", "polars": "approximate early-close rule, lazy scan", "duckdb": "approximate early-close rule, SQL window"}
137+for eng in ["RAID cpu", "RAID metal", "polars", "duckdb", "pandas"]:
138+ if not g6[eng]: continue
139+ m, lo, hi = ci(g6[eng]); e6_key[eng] = {"median_s": m, "ci": [lo, hi], "bars_per_s": e6_key["bars_per_file"] / m}; lines.append(f"{eng} & {fmt(m, 3)} & [{fmt(lo, 3)}, {fmt(hi, 3)}] & {e6_key['bars_per_file'] / m / 1e6:.1f}M & {notes[eng]} \\\\")
140+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e6.tex", "\n".join(lines)); key["E6"] = e6_key
141+
142+# ---------------------------------------------------------------- E7
143+e7 = json.load(open(os.path.join(RES, "e7_summary.json"))) if os.path.exists(os.path.join(RES, "e7_summary.json")) else []
144+inputs["e7_summary.json"] = sha(os.path.join(RES, "e7_summary.json")) if e7 else None
145+lines = [r"\begin{tabular}{llrrrllrrrr}", r"\toprule", r"Node & chip & GPU cores & CPU thr. & GB & tests (metallib / runtime) & cold / warm compile (ms) & RV fused $1000\times3900$ speedup & regression speedup & GARCH P=64 speedup & rolling speedup \\", r"\midrule"]; e7_key = {}
146+order = ["m2m8a", "M2M32c", "M2U64", "M3BA24", "M3U96b", "m4md", "m4mm", "M4M64c", "m5ma", "M5M36", "m6ma"]
147+for n in order:
148+ r = next((x for x in e7 if x.get("node") == n), None)
149+ if not r: continue
150+ t = ("pass" if r["tests_metallib"].startswith("All tests passed") else "FAIL") + " / " + ("pass" if r["tests_runtime_msl"].startswith("All tests passed") else "FAIL")
151+ e7_key[n] = {k: r.get(k) for k in ["chip", "gpu_cores", "cpu_threads", "mem_gb", "macos", "cold_compile_s", "warm_compile_s", "realized_fused@1000x3900_speedup_MT", "regression_stats@1000x3900_speedup_MT", "garch_nll_P64@1000x390_speedup_MT", "rolling_sumsq@1000x3900_speedup_MT", "realized_fused@1000x3900_cpuMT_ms", "realized_fused@1000x3900_metal_ms"]} | {"tests": t}
152+ lines.append(f"{n} & {r['chip']} & {r['gpu_cores']} & {r['cpu_threads']} & {r['mem_gb']} & {t} & {fmt(1e3 * (r['cold_compile_s'] or 0), 0)} / {fmt(1e3 * (r['warm_compile_s'] or 0), 0)} & {fmt(r['realized_fused@1000x3900_speedup_MT'])} & {fmt(r['regression_stats@1000x3900_speedup_MT'])} & {fmt(r['garch_nll_P64@1000x390_speedup_MT'])} & {fmt(r['rolling_sumsq@1000x3900_speedup_MT'])} \\\\")
153+lines += [r"\bottomrule", r"\end{tabular}"]; write("tab_e7.tex", "\n".join(lines)); key["E7"] = e7_key
154+fig, ax = plt.subplots(figsize=(6, 2.8)); xs = [e7_key[n]["gpu_cores"] for n in e7_key];
155+for k, lab, c in [("realized_fused@1000x3900_speedup_MT", "RV fused 1000×3900", COL["metal"]), ("garch_nll_P64@1000x390_speedup_MT", "GARCH NLL P=64 1000×390", COL["cpuN"]), ("rolling_sumsq@1000x3900_speedup_MT", "rolling sumsq 1000×3900", COL["cpu1"])]:
156+ ax.scatter([e7_key[n]["gpu_cores"] for n in e7_key], [e7_key[n][k] for n in e7_key], label=lab, color=c, s=18)
157+for n in e7_key: ax.annotate(e7_key[n]["chip"].replace("Apple ", ""), (e7_key[n]["gpu_cores"], e7_key[n]["realized_fused@1000x3900_speedup_MT"]), fontsize=6, xytext=(2, 2), textcoords="offset points")
158+ax.axhline(1, color="k", lw=.6, ls="--"); ax.set_xlabel("GPU cores"); ax.set_ylabel("Metal / CPU-MT speedup"); ax.set_yscale("log"); ax.grid(alpha=.3); ax.legend(); savefig(fig, "fig_e7.pdf")
159+
160+# ---------------------------------------------------------------- hardware inventory + manifest
161+env = glob.glob(os.path.join(ROOT, "experiments", "manifests", "environment_*.json")); envj = json.load(open(env[0])) if env else {}
162+key["laptop"] = {k: envj.get(k) for k in ["chip", "os", "xcode", "clang", "cmake", "metal_toolchain", "brew", "load_avg", "thermal", "ncpu", "perf_cores", "eff_cores"]}
163+key["generated_at"] = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat()
164+json.dump(key, open(os.path.join(RES, "key_numbers.json"), "w"), indent=1)
165+json.dump({"inputs": inputs, "outputs": outputs, "key_numbers_sha256": sha(os.path.join(RES, "key_numbers.json"))}, open(os.path.join(ROOT, "experiments", "manifests", "paper_assets.json"), "w"), indent=1)
166+print("tables:", sorted(outputs)); print(json.dumps({k: key[k] for k in ["breakeven", "E6"]}, indent=1)[:1500])
added experiments/scripts/run_e1_e2_e3.sh +7 −0
@@ -0,0 +1,7 @@
1+#!/bin/bash
2+set -uo pipefail; cd "$(dirname "$0")/../.."; R=experiments/results/raw; E0=experiments/results/e0_universe.json
3+rm -f $R/e1_batched_rv.jsonl $R/e2_har.jsonl $R/e3_garch.jsonl
4+echo "== E1 $(date)"; ./build/macos-release/bench_bench_e1_batched_rv $E0 $R/e1_batched_rv.jsonl 5 2> $R/e1.log; echo "E1 exit=$?"
5+echo "== E2 $(date)"; ./build/macos-release/bench_bench_e2_har $E0 $R/e2_har.jsonl 5 2> $R/e2.log; echo "E2 exit=$?"
6+echo "== E3 $(date)"; ./build/macos-release/bench_bench_e3_garch $E0 $R/e3_garch.jsonl 1000 experiments/results/e0_daily.json 2> $R/e3.log; echo "E3 exit=$?"
7+echo "== done $(date)"
modified include/raid/estimators/realized.hpp +1 −0
@@ -25,6 +25,7 @@ namespace raid::est {
25 struct RealizedOptions { 25 struct RealizedOptions {
26 bool bpv_finite_sample{true}; 26 bool bpv_finite_sample{true};
27 bool fuse_kernels{true}; // Metal: one fused pass for all accumulators (ablation: false → 4 kernels) 27 bool fuse_kernels{true}; // Metal: one fused pass for all accumulators (ablation: false → 4 kernels)
28+ bool tripower{true}; // tripower quarticity (CPU FP64, parallel over segments; |x|^{4/3} computed once per element)
28 bool annualize{false}; // multiply variances by `periods_per_year` 29 bool annualize{false}; // multiply variances by `periods_per_year`
29 f64 periods_per_year{252.0}; 30 f64 periods_per_year{252.0};
30 ExecutionPlan plan{}; 31 ExecutionPlan plan{};
added paper/appendices/appendix.tex +46 −0
@@ -0,0 +1,46 @@
1+\begin{appendices}
2+\section{Kernel pseudocode}
3+\label{app:kernels}
4+\begin{lstlisting}[basicstyle=\ttfamily\scriptsize]
5+df two_sum(float a, float b): s = a + b; bb = s - a; e = (a - (s - bb)) + (b - bb); return (s, e)
6+df df_add_f(df a, float v): (h, l) = two_sum(a.hi, v); l += a.lo; h2 = h + l; return (h2, l - (h2 - h))
7+kernel realized_fused(x, chunks, out): // one threadgroup (256 threads) per (segment, chunk)
8+ c = chunks[gid]; s2 = adj = neg = pos = s4 = (0, 0)
9+ for i in c.begin + lid .. c.end step 256:
10+ v = x[i]; q = v*v; s2 += q; s4 += q*q; if v < 0: neg += q elif v > 0: pos += q
11+ if i > c.begin: adj += |v| * |x[i-1]|
12+ tree-reduce each df over the threadgroup in fixed order; thread 0 writes 5 float2
13+host: for each chunk c of segment s: acc[s] += (double)hi + (double)lo (fixed order);
14+ adj[s] += |x[c.begin]| * |x[c.begin-1]| in FP64 when c.begin > segment begin
15+kernel garch_nll(r, seg, params[8], var0, jobs, out, model): // one thread per (segment, parameter vector)
16+ (mu, omega, alpha, gamma, beta, nu, kconst, Eabs) = params[job]; s2 = var0[seg]
17+ validity checks -> +inf; nll = (0, 0); e_prev = r[b] - mu
18+ for i in 0..n-1: if i > 0: s2 = recursion(model); if !(s2 > 0): return +inf
19+ e = r[b+i] - mu; ll = kconst - 0.5 log s2 - (student ? 0.5 (nu+1) log(1 + e^2/((nu-2) s2)) : 0.5 e^2/s2)
20+ nll += -ll
21+ out[job] = nll.hi + nll.lo // host adds n log(sd) for the per-series scaling
22+\end{lstlisting}
23+
24+\section{Optimiser details}
25+GARCH-family QML: parameters are transformed to an unconstrained space ($\omega = e^{u}$; persistence $p = \sigma(u_p)(1-10^{-9})$ and share $s = \sigma(u_s)$ so that $\alpha = sp$, $\beta = (1-s)p$ for GARCH; for GJR $\alpha+\gamma = e^{u_g}$ and $\beta = p - \alpha - \gamma/2 \ge 0$; for EGARCH $\omega$ free and $\beta = \tanh(u)$; $\nu = 2.05 + 497.95\,\sigma(u_\nu)$). BFGS with Armijo backtracking (factor 0.5, up to 60 halvings), central-difference gradients with step $10^{-5}\max(1,|u|)$, convergence when $\|g\| < 10^{-5}$, or when the relative change of the negative log-likelihood is below $10^{-8}$ (or the step below $10^{-9}$) with $\|g\| < 10^{-2}$ in the transformed space; at most 500 iterations; up to three starting variants (persistence 0.95, 0.80, 0.985; opposite EGARCH $\gamma$ sign). Covariance from the numerical Hessian (central differences, step $10^{-4}\max(1,|\theta|)$) and the OPG of per-observation scores. Returns are scaled to unit variance before optimisation and the estimates un-scaled (EGARCH: $\omega \to \omega + (1-\beta)\log s^{-2}$).
26+
27+\section{Finite-sample conventions}
28+Regular session = 09:30 to 16:00 ET inclusive of the 16:00 closing print (391 one-minute bars; 211 on 13:00 early closes); extended session 04:00--19:59 ET (17:00 on early closes). Intraday returns are close-to-close within the session ($M = $ bars $-1$). BPV factor $M/(M-1)$ on by default; TriPV factor $M/(M-2)$; RQ $= (M/3)\sum r^4$. HAR windows of 1, 5 and 22 trading days; targets averaged over $h$ sessions; HAC lags $\max(h-1, \lfloor 4(n/100)^{2/9}\rfloor)$. HARQ regressor $(\sqrt{\RQ_t} - \sqrt{\overline{\RQ}})\RV_t$ with $\overline{\RQ}$ over the estimation sample. GARCH $\sigma_0^2$ = sample variance of $\varepsilon$. Stationary bootstrap mean block $n^{1/3}$ unless stated.
29+
30+\section{Benchmark configuration}
31+Laptop: Apple M5 Max (6P+12E cores reported by \texttt{hw.perflevel}, 18 hardware threads), 40-core GPU, 48\,GB, macOS 27.0 (26A5425a), Xcode 27.2 (27B52), Apple clang 21.0.0 (clang-2100.4.1.1), Metal toolchain 32023.922, CMake 4.3.2, \texttt{-O3 -mcpu=native -ffp-model=precise}, Eigen 5.0.1 + Accelerate, Arrow 26.0.0. Metal: 256-thread threadgroups for reductions, 4\,096-element chunks, 64-thread threadgroups for GARCH jobs, shared storage mode, \texttt{fastMathEnabled = NO}. Repetitions: 5 (laptop), 3 (nodes); 2 warm-ups; shuffled order with seed 20261102; medians and 2\,000-draw bootstrap 95\,\% CIs. Cluster bundle: \texttt{-mcpu=apple-m1}, deployment target macOS 26.0, Arrow disabled, Metal enabled, prebuilt metallib plus embedded MSL.
32+
33+\section{Hardware inventory (E7)}
34+\begin{center}\scriptsize
35+\begin{tabular}{llrrrl}
36+\toprule Node & Chip & GPU cores & CPU threads & RAM (GB) & macOS \\ \midrule
37+m2m8a & Apple M2 & 10 & 8 & 8 & 26.5.1 \\ M2M32c & Apple M2 Max & 30 & 12 & 32 & 26.5.1 \\ M2U64 & Apple M2 Ultra & 60 & 24 & 64 & 26.5 \\ M3BA24 & Apple M3 & 10 & 8 & 24 & 26.5.1 \\ M3U96b & Apple M3 Ultra & 60 & 28 & 96 & 26.4 \\ m4md & Apple M4 & 10 & 10 & 16 & 26.6.2 \\ m4mm & Apple M4 Pro & 16 & 12 & 48 & 26.5.1 \\ M4M64c & Apple M4 Max & 40 & 16 & 64 & 26.4 \\ m5ma & Apple M5 Pro & 16 & 15 & 24 & 27.0 \\ M5M36 & Apple M5 Max & 32 & 18 & 36 & 27.0 \\ m6ma & Apple M6 & 12 & 12 & 24 & 27.0 \\ \bottomrule
38+\end{tabular}
39+\end{center}
40+
41+\section{API provenance}
42+FinData OS API \texttt{https://findata.spboucher.ai/api/v1} (OpenAPI 3.1 snapshot of 2026-11-02 in \texttt{docs/evidence/}); datasets \texttt{market\_bars.stock.1m.raw} (version 17\,375 at export time; 6.70 billion rows, 16\,195 symbols, 2000--2026) and \texttt{market\_bars.stock.1d.raw}; bars schema v1 (\texttt{timestamp\_utc} = bar open, UTC); exports as Parquet with the dataset version in the job record; request identifiers of every exchange are kept in the cache manifests; API key identified by a SHA-256 fingerprint only. Data © FirstRate Data, private-use licence, no redistribution.
43+
44+\section{Numerical tolerance tables}
45+See Table~\ref{tab:validation} for the reference tolerances. GPU/CPU tolerances by primitive: realized accumulators $10^{-6}$ relative ($\Sigma x^4$: $10^{-5}$); rolling sums $10^{-5}$; regression statistics $10^{-6}$ absolute on $\max(1,|\cdot|)$ and $10^{-5}$ on the solved coefficients; batched GARCH NLL $2\times10^{-5}$ relative (Student-$t$: $5\times10^{-5}$) on simulated series with $n = 2\,000$; on the E3 real-data grid the maximum relative difference is $2\times10^{-3}$ at boundary grid points and the argmin agrees for 888/888 series.
46+\end{appendices}
added paper/figures/fig_e1.pdf +0 −0

Binary file not shown.

added paper/figures/fig_e7.pdf +0 −0

Binary file not shown.

added paper/figures/fig_primitives_throughput.pdf +0 −0

Binary file not shown.

added paper/sections/applications.tex +37 −0
@@ -0,0 +1,37 @@
1+\section{Financial applications}
2+\label{sec:apps}
3+
4+The applications are not forecasting studies; they show that the batched estimators deliver the same statistical content as per-series estimation on real data, and where batching pays.
5+
6+\paragraph{E2 --- HAR forecasting and batched estimation.} For the first 100 symbols of the universe, HAR-RV, log-HAR and HARQ are re-estimated every day on a causal 120-session window and forecast the average realized variance over the next $h \in \{1, 5, 22\}$ sessions; the benchmark is the random-walk forecast $\widehat{\RV}_{t+h} = \RV_t$ (Table~\ref{tab:e2acc}). With only one year of data the windows are short, yet the median QLIKE of HAR-RV is 0.196, 0.146 and 0.134 at $h = 1, 5, 22$ against 0.251, 0.239 and 0.301 for the random walk, and HAR beats the random walk for 84\,\%, 86\,\% and 78\,\% of symbols; log-HAR is best at the longer horizons (0.138 and 0.112), consistent with the literature. Table~\ref{tab:e2rt} reports the runtime of estimating HAR-RV ($h = 1$, 252 daily observations per series) on 10, 100 and 781 series: per-series QR fits take 0.08--6.0\,ms on one thread and 0.05--0.68\,ms on 18 threads; the batched path (sufficient statistics on the backend, FP64 solve) takes 0.10--3.9\,ms on the CPU and 0.32--4.7\,ms on Metal. With 252 observations per series the designs are tiny ($252\times4$ per series), the GPU is launch-bound, and the per-series multithreaded QR is the fastest option; batching would pay only with long daily histories or many more series. Coefficients from the batched path agree with per-series QR to $10^{-9}$ (CPU) and $10^{-4}$ relative (Metal, FP32 sufficient statistics), the documented tolerance.
7+
8+\begin{table}[t]
9+\centering\small
10+\caption{E2 --- rolling causal HAR forecasts, 100 symbols, 2024 (120-session window, median QLIKE across symbols).}
11+\label{tab:e2acc}
12+\input{tables/tab_e2_accuracy}
13+\end{table}
14+\begin{table}[t]
15+\centering\small
16+\caption{E2 --- runtime of HAR-RV estimation ($h=1$), milliseconds (medians of 5 repetitions).}
17+\label{tab:e2rt}
18+\input{tables/tab_e2_runtime}
19+\end{table}
20+
21+\paragraph{E3 --- GARCH-family panel.} GARCH(1,1), GJR, EGARCH and GARCH-$t$ are fitted by QML to the daily close-to-close log returns of 888 symbols (2015--2024 daily bars, median 2\,459 training observations; the last 40 sessions of 2024 are held out). Table~\ref{tab:e3} reports convergence rates, iterations, time per fit, persistence, and the QLIKE of the fixed-parameter one-day-ahead variance forecasts against the next day's realized variance. GARCH and GARCH-$t$ converge for all 888 series, GJR for 98.4\,\% and EGARCH for 92.8\,\% (after up to two restarts; the failures are reported as such, not as estimates), in 2.9--18\,ms per fit; persistence medians are 0.956--0.973. Daily GARCH forecasts have a median QLIKE of 0.39--0.40 against 0.27--0.28 for the realized-variance random walk: a one-day-ahead forecast built from intraday data beats one built from daily returns, as the literature since \citet{AndersenBollerslevDieboldLabys2003} would predict, and the gap is not a numerical artefact. The GPU's contribution in E3 is the likelihood grid: evaluating 256 $(\omega,\alpha,\beta)$ combinations per series for all 888 series takes 23\,ms on Metal against 117\,ms on 18 CPU threads (5.1$\times$); the grid argmin is identical on both backends for all 888 series, with a maximum relative difference of $2\times10^{-3}$ in the likelihood at the least-likely grid points (near the non-stationarity boundary).
22+
23+\begin{table}[t]
24+\centering\small
25+\caption{E3 --- GARCH-family QML on 888 daily return series (2015--2024); forecasts evaluated on the last 40 sessions of 2024 against realized variance.}
26+\label{tab:e3}
27+\input{tables/tab_e3_fits}
28+\end{table}
29+
30+\paragraph{E4 --- realized covariance and connectedness.} For $N \in \{10, 30, 60, 100\}$ symbols with at least 95\,\% regular-bar coverage, daily realized covariance matrices are formed from synchronised 5-minute returns (78 per day) on the days where every symbol has the full grid, and rolling Diebold--Yilmaz total connectedness is computed from a VAR(1) on the daily log-RV panel (120-day windows, $H = 10$). Table~\ref{tab:e4} shows the two constraints the charter asked for. Memory is not one of them: the rolling connectedness over 133 windows takes 0.5, 2.3, 8.9 and 29\,ms for $N = 10, 30, 60, 100$ on the CPU and never fails. Conditioning is: the median condition number of the daily realized covariance grows from 81 ($N = 10$) to 422 ($N = 30$) and $5.9\times10^3$ ($N = 60$), and the matrix is singular for $N = 100 > 78$ bins; the Ledoit--Wolf shrinkage intensity rises from 0.16 to 0.33 accordingly. Strict synchronisation also discards 70--164 of the 252 days as $N$ grows. Connectedness itself is not GPU work at these sizes; the GPU-relevant part is the realized covariance of large panels, which reduces to the batched sufficient statistics of E1.
31+
32+\begin{table}[t]
33+\centering\small
34+\caption{E4 --- daily realized covariance (5-minute, synchronised) and rolling Diebold--Yilmaz connectedness, 2024.}
35+\label{tab:e4}
36+\input{tables/tab_e4}
37+\end{table}
added paper/sections/architecture.tex +34 −0
@@ -0,0 +1,34 @@
1+\section{Architecture and API}
2+\label{sec:arch}
3+
4+\raid{} is a C++20 library (MIT licence) with four layers (Figure~\ref{fig:arch}): a \emph{core} of strong types and infrastructure, a \emph{data} layer that turns provider bars into validated, calendar-aware return panels, a \emph{statistics/estimators/models} layer that implements the registry, and a \emph{backend} layer that exposes a small set of batched numerical primitives with a CPU reference implementation and a Metal implementation. Python bindings (nanobind) and a command-line tool sit on top.
5+
6+\begin{figure}[t]
7+\centering
8+\begin{lstlisting}[basicstyle=\ttfamily\scriptsize]
9+ Python (nanobind, GIL released) raid.api / raid.findata CLI: raid devices|validate|estimate|data
10+ ------------------------------------------------------------------------------------------------------
11+ core types (Timestamp us UTC, Date, Interval, Backend, Precision) | RaidError | log (redaction)
12+ rng (xoshiro256++ streams) | ThreadPool | SHA-256 | Estimate / Provenance / ExecutionPlan / Manifest
13+ data MarketCalendar (XNYS rules, DST, early closes) | BarSeries + QualityReport | returns (session-aware)
14+ aggregate (calendar-aware bins) | io: CSV fixtures, Parquet/Arrow IPC (projection) | FinData client
15+ estimators reductions (Neumaier/pairwise/Welford) | rolling | covariance (HC, HAC, cluster) | OLS/WLS/ridge
16+ realized (RV, BPV, RSV, RQ, J, TriPV, RCov) | HAR family | GARCH family (QML) | bootstrap | VAR/GFEVD/DY
17+ backend Batch = values + CSR offsets -> sum_squares | sum_abs_adjacent | semivariances | sum_fourth
18+ rolling_sum_squares | regression_stats (X'X, X'y, y'y) | garch_nll_batch [CpuBackend FP64 | MetalBackend FP32]
19+\end{lstlisting}
20+\caption{Layers of \raid{}. Estimators consume the backend interface through an \texttt{ExecutionPlan} (\texttt{auto|cpu|metal}); every result is an \texttt{Estimate} carrying coefficients, loss, inference, convergence, warnings, runtime, backend, precision and provenance.}
21+\label{fig:arch}
22+\end{figure}
23+
24+\paragraph{Core types.} Timestamps are microseconds since the Unix epoch in UTC and are never mixed with raw integers; dates use a proleptic-Gregorian day count; intervals are an enumeration. Every estimator returns an \texttt{Estimate}: named coefficients, a loss (residual sum of squares or negative log-likelihood, documented per model), an optional \texttt{InferenceResult} (standard errors, $t$-statistics, $p$-values, covariance, covariance type), a \texttt{Convergence} record (status, iterations, function evaluations, gradient norm, message), warnings, runtime, the backend and precision actually used, and a \texttt{Provenance} record (dataset identifier and version, request identifiers, input hashes, code version). \texttt{Estimate::ok()} requires convergence; a failed optimisation is never reported as a success. An \texttt{ExecutionPlan} selects the backend, the precision policy (FP32 chunk length, compensated accumulation, validation tolerance), determinism, threads and fallback behaviour.
25+
26+\paragraph{Data layer.} \texttt{BarSeries} stores OHLCV columns, bar-open timestamps and a session index. The \texttt{MarketCalendar} implements the NYSE rules (weekday holidays with the Saturday/Sunday observance rules, Good Friday from the Gregorian Easter, Juneteenth since 2022, special closures, early closes at 13:00, US daylight-saving transitions before and after 2007) and is validated set-for-set against \texttt{exchange\_calendars} over 2000--2026. \texttt{validate()} counts OHLC inconsistencies, non-positive prices, grid misalignment, bars outside the session policy, non-trading days, duplicates, price jumps and the number of regular bars missing relative to the calendar (391 on a normal day, 211 on an early close, the 16:00 closing print included); prices are never altered and bars are never manufactured. Returns are computed within sessions only; the overnight return and the daily open-to-close and close-to-close returns are separate outputs. Aggregation to coarser intervals bins bars from the session open and keeps the closing print as its own bin, which reproduces the provider's derived 5-minute series exactly (Section~\ref{sec:setup}).
27+
28+\paragraph{Panels and batches.} Batched kernels take a \emph{CSR batch}: one contiguous array of values and an offsets array delimiting segments (a segment is a session, a series, or a window). Ragged panels---different session lengths, early closes, delisted series---are therefore first-class, and no padding is needed. Rectangular panels get a dense column-major view for linear algebra.
29+
30+\paragraph{Backend contract.} The backend interface has seven primitives (Figure~\ref{fig:arch}). Each has an FP64 CPU implementation (compensated per-segment sums, parallel over segments) and a Metal implementation. Estimators are backend-independent: \texttt{realized\_measures\_batch}, \texttt{har\_batch} and the GARCH grid screen call the primitives and finish in FP64 on the CPU (finite-sample constants, solves, inference). \texttt{resolve(plan)} maps \texttt{auto} to Metal when a device is available and falls back to the CPU otherwise, recording the reason.
31+
32+\paragraph{FinData client.} The data provider is FinData~OS, a private lakehouse serving FirstRate Data bars through a versioned REST API (Section~\ref{sec:setup}). The client authenticates with a bearer key read from the environment only, redacts secrets from logs, applies a token bucket (5 requests per second against a 600 per minute limit), retries with exponential backoff and \texttt{Retry-After}, follows cursor pagination of Arrow IPC pages, chunks requests to the documented caps (31 days of 1-minute bars per call, 12 years per export), polls asynchronous Parquet exports, and keeps a checksum-verified cache keyed by dataset version. Every exchange's request identifier and dataset version are kept for provenance.
33+
34+\paragraph{Python and CLI.} The nanobind module releases the GIL around every call, accepts NumPy arrays as views and returns NumPy arrays and JSON-like dictionaries. \texttt{raid.findata} exposes the client; \texttt{raid.api} exposes the estimators with a \texttt{backend} argument. The CLI offers \texttt{raid devices}, \texttt{raid validate}, \texttt{raid estimate realized|har|garch|ols} and \texttt{raid data catalog|bars|export|sessions|coverage|query}.
added paper/sections/conclusion.tex +6 −0
@@ -0,0 +1,6 @@
1+\section{Conclusion}
2+\label{sec:concl}
3+
4+Can high-frequency econometric workloads be moved to an FP32-only GPU without losing accuracy, inference or reproducibility? For the estimators studied here the answer is yes on accuracy and reproducibility and ``sometimes'' on speed. Compensated double-float accumulation, chunked FP64 combination and mixed-precision design keep Metal results within $10^{-6}$ of FP64 for realized measures and regression statistics and within $2\times10^{-5}$ for batched GARCH likelihoods, bitwise reproducible across runs and thread counts, with Monte Carlo coverage and reference comparisons indistinguishable from the CPU path. The speed answer is specific: the fused realized kernel beats an 18-thread CPU beyond about $4\times10^4$ elements (2--3.4$\times$ on raw reductions, 1.1--1.4$\times$ once the estimator's own FP64 post-processing is counted), batched GARCH likelihood grids gain 5--8$\times$, rolling sums and single-vector recursions lose, small panels tie, and the pipeline from Parquet to estimates is governed by column projection and calendar logic rather than by any kernel. Across eleven Apple Silicon generations the GPU helps most where the CPU is weakest.
5+
6+The practical lesson for econometric software on unified-memory machines is that the device is cheap and the conversion is not: a library that wants GPU speedups must keep FP32 copies resident across calls, fuse its passes, and reserve the GPU for problems whose parallelism is in the number of series or parameter vectors, not in the length of a recursion. The methodological lesson is that none of this requires new estimators---only explicit conventions, references and tolerances, which is where most implementations differ.
added paper/sections/correctness.tex +41 −0
@@ -0,0 +1,41 @@
1+\section{Statistical correctness}
2+\label{sec:correct}
3+
4+Every Tier~A method satisfies five requirements: an independent FP64 CPU reference, an analytical or simulation truth where one exists, agreement with established software under identical conventions, GPU/CPU agreement within a documented tolerance, and adversarial tests. Table~\ref{tab:validation} summarises the reference comparisons on committed synthetic fixtures (no vendor data); every row is a test in the repository that runs on every build.
5+
6+\begin{table}[t]
7+\centering\small
8+\begin{threeparttable}
9+\caption{Reference validation on synthetic fixtures (all tests pass).}
10+\label{tab:validation}
11+\begin{tabular}{lllr}
12+\toprule
13+Method & Reference (version) & Quantity compared & Tolerance \\
14+\midrule
15+XNYS calendar & \texttt{exchange\_calendars} 4.13.2 & sessions, holidays, early closes 2000--2026 (6\,790 sessions) & exact sets \\
16+Session aggregation 1m$\to$5m & FinData derived 5m (live) & OHLCV of 280 regular 5-minute bars (AAPL, Nov.~2024) & exact \\
17+RV, BPV, RSV$^\pm$, RQ & \texttt{highfrequency} 1.0.3 / NumPy & per-session values (10 sessions) & $10^{-10}$ / $10^{-12}$ rel. \\
18+OLS, HC0--HC3, HAC, cluster & \texttt{statsmodels} 0.15.0 & coefficients, SE, $p$-values, $R^2$, SSR & $10^{-10}$ / $10^{-9}$ \\
19+$t$ $p$-values & \texttt{scipy} 1.18.1 & $2\,t.\mathrm{sf}$ & $10^{-12}$ \\
20+HAR-RV $h=1,5,22$, log-HAR, HARQ (HAC) & \texttt{statsmodels} 0.15.0; \texttt{highfrequency} \texttt{HARmodel} & coefficients, HAC SE & $10^{-9}$; $10^{-8}$ ($h=1$), $10^{-6}$ (HARQ, $h=5$) \\
21+GARCH, GJR, EGARCH, GARCH-$t$ & \texttt{arch} 8.0.0; \texttt{rugarch} 1.5.6 & log-likelihood; parameters; $h\le5$ forecasts & $5\times10^{-4}$ / $2\times10^{-4}$; 5\,\% + 0.01; 5\,\% \\
22+VAR(2), FEVD, GFEVD, DY, Ledoit--Wolf & \texttt{statsmodels} 0.15.0 (GFEVD by hand) & coefficients, $\Sigma_u$, llf, FEVD, GFEVD, totals, shrinkage & $10^{-9}$ ($10^{-7}$ totals) \\
23+\midrule
24+Metal vs CPU FP64 & \raid{} CPU backend & RV, BPV, RSV$^\pm$ ($n$ = 390--39\,000, chunked) & $10^{-6}$ rel. ($\Sigma x^4$: $10^{-5}$) \\
25+ & & rolling sums; $X'X$, $X'y$, $y'y$ and the solve & $10^{-5}$; $10^{-6}$ (solve $10^{-5}$) \\
26+ & & batched GARCH NLL (GJR, Student-$t$); argmin over a grid & $2\times10^{-5}$ ($5\times10^{-5}$); identical \\
27+\bottomrule
28+\end{tabular}
29+\begin{tablenotes}\footnotesize
30+\item Conventions converted before comparison: \texttt{rQuar} uses $(M+1)/3$; \texttt{rBPCov(makeReturns=FALSE)} applies no $M/(M-1)$; HARQ uses $(\sqrt{\RQ_t}-\sqrt{\overline{\RQ}})\RV_t$; \texttt{rugarch} EGARCH \texttt{alpha1}/\texttt{gamma1} are swapped relative to \eqref{eq:egarch}; \texttt{arch} initialises $\sigma_0^2$ by backcasting. Student-$t$ $\nu$ is compared only when identified ($\nu < 100$).
31+\end{tablenotes}
32+\end{threeparttable}
33+\end{table}
34+
35+\paragraph{Monte Carlo.} Table~\ref{tab:e5} (experiment E5, $R = 1\,000$ replications) reports bias, RMSE and the empirical coverage of 95\,\% intervals. For GARCH(1,1) with $(\omega,\alpha,\beta) = (0.02, 0.08, 0.90)$ and $n = 2\,000$, biases are at most $0.005$ in absolute value, coverage of the QMLE intervals is 0.93--0.97, and all 1\,000 fits converge; at $n = 500$ the familiar finite-sample downward bias of $\beta$ ($-0.026$) and under-coverage of $\alpha$ (0.90) appear. For HAR with overlapping $h = 5$ targets and HAC inference, coverage of the pseudo-true projection coefficients is 0.90--0.94 ($T = 1\,200$), the usual mild under-coverage of HAC. Realized variance converges at the $\sqrt{2/M}$ rate: the Monte Carlo RMSE is within 10\,\% of $\sqrt{2/M}\,\mathrm{IV}$ for $M \in \{39, 390, 3\,900\}$. The stationary-bootstrap interval for the mean of an AR(1) covers in 89\,\% of replications ($R = 100$, $n = 800$, $b = 10$).
36+
37+\input{tables/tab_e5}
38+
39+\paragraph{Reproducibility.} With the same seed, the 2\,000 bootstrap statistics are bitwise identical when computed with 1, 4 or 18 threads; the CPU realized measures are bitwise identical with 1 or 18 threads; two Metal runs give bitwise identical RV, BPV and RQ; and the maximum relative difference between Metal and CPU FP64 realized variance on 50 segments of Student-$t(5)$ returns is $3.7\times10^{-8}$, consistent with the input-rounding bound of Section~\ref{sec:metal}.
40+
41+\paragraph{Adversarial cases.} The test suite covers zero and missing volume, holidays, DST transitions (both rules), the 16:00 print, early closes, duplicated and unsorted bars, rank-deficient and ill-conditioned designs (warned, minimum-norm solution), singular $X'X$ from sufficient statistics (pseudo-inverse, warned), invalid GARCH parameters ($+\infty$ likelihood on both backends), non-convergence (never reported as success), insufficient samples (typed exceptions), nonexistent Metal devices and forced fallback. Numerical gradients of the GARCH likelihood agree across step sizes $10^{-4}$, $10^{-5}$ and $10^{-6}$ to $10^{-3}$ relative.
added paper/sections/endtoend.tex +26 −0
@@ -0,0 +1,26 @@
1+\section{End-to-end benchmarks}
2+\label{sec:e2e}
3+
4+\paragraph{E1 --- batched realized measures on the real panel.} Table~\ref{tab:e1} and Figure~\ref{fig:e1} report the full estimator path (\texttt{realized\_measures\_batch}: the five fused accumulators, finite-sample constants, jump proxy, tripower quarticity and daily returns) on the 2024 panel, with segments of one day, one week, one month or one year of regular-session 1-minute returns, for panel widths of 1, 10, 100 and 926 symbols, and on a synthetic 10\,000-series panel. Three facts stand out. First, at the estimator level the GPU advantage is much smaller than at the kernel level: on 40 million real returns the 18-thread CPU needs 29--30\,ms and Metal 20--28\,ms (1.07--1.41$\times$), and at $4.8\times10^6$ returns or fewer the two are statistically tied or the CPU wins; the single-thread CPU is 8$\times$ slower than either. The CPU post-processing that the GPU does not remove---conversion to FP32, tripower in FP64, the per-segment finishing---bounds the achievable speedup (Amdahl's law), and the fused kernel's 2--3$\times$ on raw reductions shrinks accordingly. Second, kernel fusion matters more than the device: the four-kernel Metal variant is 1.4--3.2$\times$ slower than the fused one on the same data, because each extra pass re-reads the panel and pays its own launch and synchronisation. Third, accuracy is unaffected: the maximum relative difference between Metal and FP64 realized variance over all 926$\times$252 session segments is $1.6\times10^{-7}$ and decreases with segment length ($1.4\times10^{-8}$ for yearly segments), exactly the input-rounding behaviour predicted in Section~\ref{sec:metal}. Longer segments favour the GPU (month and year segments are the Metal wins); many tiny segments favour the CPU.
5+
6+\begin{table}[t]
7+\centering\scriptsize
8+\caption{E1 --- batched realized measures (estimator path), laptop. Medians of 5 repetitions; ``real'' = 2024 FinData panel; ``synthetic'' = Student-$t(5)$ returns; max rel.\ err.\ = largest $|\RV_{\text{Metal}}-\RV_{\text{FP64}}|/\RV_{\text{FP64}}$ over segments.}
9+\label{tab:e1}
10+\input{tables/tab_e1}
11+\end{table}
12+
13+\begin{figure}[t]
14+\centering\includegraphics[width=.95\textwidth]{figures/fig_e1.pdf}
15+\caption{E1 --- throughput (returns per second) of the realized-measures estimator by backend on the real and synthetic panels.}
16+\label{fig:e1}
17+\end{figure}
18+
19+\paragraph{E6 --- the Arrow/Parquet-to-estimator pipeline.} Table~\ref{tab:e6} compares, on four of the cached export files (2.2--2.5 million bars, 50 symbols, one year each), the \raid{} pipeline---Parquet read with column projection and threaded decoding, session filtering with the exact calendar, within-session log returns, and the five realized measures per session---with the same computation written in pandas, Polars (lazy scan) and DuckDB (SQL window functions). The baselines use an approximate early-close rule (fixed dates) because none of them ships an exchange calendar. \raid{} on the CPU processes a file in 0.118\,s (20 million bars per second), Polars in 0.165\,s and DuckDB in 0.164\,s (both 14 million bars per second), pandas in 2.56\,s. Within \raid{}, decoding the Parquet columns takes 0.065\,s, session filtering and returns 0.021\,s and the estimators 0.031\,s on the CPU; Metal makes the estimator stage slower (0.037\,s) because the stage is below the break-even size of E1 and the conversion is paid again. The answer to RQ3 is therefore moderate: a typed, projected, calendar-exact C++ pipeline is about 1.4$\times$ faster than the best columnar engines and 22$\times$ faster than pandas on this task, and the GPU plays no role in it. Before column projection was added, the same pipeline took 0.35\,s---reading eight of twenty columns instead of all of them, and avoiding the per-row string column, was worth more than any kernel. True zero-copy from Arrow to the estimators was possible for every file (single-chunk, null-free numeric columns), but the session filter necessarily materialises a selection; we count four column copies per file.
20+
21+\begin{table}[t]
22+\centering\small
23+\caption{E6 --- end-to-end pipeline per export file (median of 4 files $\times$ 3 repetitions; 2.37 million bars per file).}
24+\label{tab:e6}
25+\input{tables/tab_e6}
26+\end{table}
added paper/sections/estimators.tex +38 −0
@@ -0,0 +1,38 @@
1+\section{Estimator definitions and conventions}
2+\label{sec:est}
3+
4+The method registry (\texttt{docs/methods/registry.md} in the repository) records, for each method, the equation, the inputs, the reference implementation and its options, the test tolerance and the validation status. We reproduce the Tier~A definitions here because conventions---not formulas---are where implementations diverge.
5+
6+\paragraph{Returns and sessions.} With bar closes $p_{t,0},\dots,p_{t,M_t}$ in session $t$ (regular hours 09:30--16:00 ET inclusive of the 16:00 closing print), intraday log returns are $r_{t,j} = \log(p_{t,j}/p_{t,j-1})$, $j = 1,\dots,M_t$; a normal session has $M_t = 390$ and an early close $M_t = 210$ when no minute is missing. Zero-volume minutes are absent at the source and are left absent.
7+
8+\paragraph{Realized measures.} For one session with $M$ returns,
9+\begin{align}
10+\RV &= \sum_{j=1}^{M} r_j^2, \qquad
11+\BPV = \mu_1^{-2}\,\frac{M}{M-1}\sum_{j=2}^{M} |r_j|\,|r_{j-1}|, \quad \mu_1 = \sqrt{2/\pi}, \label{eq:rv}\\
12+\RSV^{-} &= \sum_{j} r_j^2\,\I\{r_j<0\}, \qquad \RSV^{+} = \sum_{j} r_j^2\,\I\{r_j>0\}, \qquad
13+\RQ = \frac{M}{3}\sum_{j=1}^{M} r_j^4, \label{eq:rq}\\
14+J &= \max(\RV-\BPV, 0), \qquad
15+\mathrm{TriPV} = \mu_{4/3}^{-3}\,\frac{M}{M-2}\sum_{j=3}^{M} |r_j r_{j-1} r_{j-2}|^{4/3}, \label{eq:tripv}
16+\end{align}
17+with $\mu_p = \E|Z|^p$ for $Z\sim N(0,1)$. The finite-sample factors are options. Two conventions of \texttt{highfrequency} differ: \texttt{rBPCov} with \texttt{makeReturns = FALSE} applies no $M/(M-1)$ factor, and \texttt{rQuar} uses $(M+1)/3$, the number of prices, instead of $M/3$; the reference tests compare after conversion. Realized covariance on synchronised returns is $\sum_j r_j r_j'$ without demeaning.
18+
19+\paragraph{Regression and inference.} OLS, WLS and ridge are solved by column-pivoting Householder QR (SVD when rank-deficient; ridge by augmentation). Covariances follow \texttt{statsmodels}: classical $s^2(X'X)^{-1}$; HC0--HC3 with the leverage corrections; HAC with Bartlett weights and $L$ lags (default $\lfloor 4(n/100)^{2/9}\rfloor$, no small-sample correction); one-way cluster with $\frac{G}{G-1}\frac{n-1}{n-k}$. $p$-values use $t(n-k)$. Rolling OLS refits each window.
20+
21+\paragraph{HAR family.} With daily $\RV_t$, lags $(1,5,22)$ and horizon $h$,
22+\begin{equation}
23+y_{t+h} = \beta_0 + \beta_d \RV_t + \beta_w \overline{\RV}^{(5)}_t + \beta_m \overline{\RV}^{(22)}_t \;[+\,\beta_q(\sqrt{\RQ_t}-\sqrt{\overline{\RQ}})\RV_t] + \varepsilon_{t+h}, \qquad \overline{\RV}^{(k)}_t = \tfrac1k\sum_{j=0}^{k-1}\RV_{t-j},
24+\label{eq:har}
25+\end{equation}
26+where $y_{t+h}$ is the average of $\RV_{t+1},\dots,\RV_{t+h}$ (the single-day target is an option) and the bracketed HARQ term follows \citet{BollerslevPattonQuaedvlieg2016} in the demeaned form used by \texttt{highfrequency}; log-HAR applies the same regression to $\log\RV$ and back-transforms with $\exp$ (the smearing factor is reported, not applied). Overlapping targets for $h>1$ induce MA$(h-1)$ errors, so the HAC lag is at least $h-1$. Rolling estimation is strictly causal: the model fitted on observations up to $t$ forecasts $y_{t+h}$. Forecast losses are MSE and QLIKE $= y/\hat y - \log(y/\hat y) - 1$ \citep{Patton2011}.
27+
28+\paragraph{GARCH family.} With $\varepsilon_t = r_t - \mu$,
29+\begin{align}
30+\text{GARCH}(1,1):\;& \sigma_t^2 = \omega + \alpha\varepsilon_{t-1}^2 + \beta\sigma_{t-1}^2, \qquad
31+\text{GJR}:\; \sigma_t^2 = \omega + (\alpha + \gamma\I\{\varepsilon_{t-1}<0\})\varepsilon_{t-1}^2 + \beta\sigma_{t-1}^2, \label{eq:garch}\\
32+\text{EGARCH}:\;& \log\sigma_t^2 = \omega + \alpha(|z_{t-1}| - \E|z|) + \gamma z_{t-1} + \beta\log\sigma_{t-1}^2, \quad z = \varepsilon/\sigma. \label{eq:egarch}
33+\end{align}
34+Errors are Gaussian or standardised Student-$t$ with $\nu\in(2.05, 500)$ (the bounds of \texttt{arch}); $\sigma_0^2$ is the sample variance of $\varepsilon$ (as \texttt{rugarch}; \texttt{arch} uses a backcast, which explains the $5\times10^{-4}$ log-likelihood tolerance against it). Estimation is quasi-maximum likelihood by BFGS on transformed parameters (log for $\omega$, logistic for persistence and the $\alpha/\beta$ share, $\tanh$ for the EGARCH $\beta$), with central-difference gradients validated across step sizes; up to two alternative starting points are tried on non-convergence, and the number tried is reported. Standard errors are the Bollerslev--Wooldridge sandwich $H^{-1}\,\mathrm{OPG}\,H^{-1}$ by default (Hessian and OPG are options). Forecasts are analytical for GARCH/GJR and by seeded simulation beyond one step for EGARCH. The \texttt{rugarch} EGARCH maps its \texttt{alpha1} to our $\gamma$ and its \texttt{gamma1} to our $\alpha$.
35+
36+\paragraph{Bootstrap.} The stationary bootstrap of \citet{PolitisRomano1994} draws blocks of geometric length (mean $b$, default $n^{1/3}$) starting at uniform indices with circular wrap; the circular block bootstrap uses fixed length; an iid option exists for comparison only. Replica $r$ uses the stream $(\text{seed}, r)$ of a counter-seeded xoshiro256++ generator, so replicas are independent of the thread that computes them.
37+
38+\paragraph{Tier B.} VAR$(p)$ with intercept by multivariate least squares, generalized FEVD \citep{KoopPesaranPotter1996,PesaranShin1998} normalised row-wise, Diebold--Yilmaz total, from, to and net connectedness \citep{DieboldYilmaz2012,DieboldYilmaz2014} with rolling windows, and Ledoit--Wolf shrinkage toward the scaled identity \citep{LedoitWolf2004}. Realized GARCH, HEAVY, DCC, GARCH-MIDAS, state-space and change-point methods, pre-averaging, realized kernels and order-flow methods are \emph{not} implemented; the registry says so.
added paper/sections/metal.tex +16 −0
@@ -0,0 +1,16 @@
1+\section{Metal execution and precision strategy}
2+\label{sec:metal}
3+
4+\paragraph{The constraint.} Apple GPUs and the Metal Shading Language have no 64-bit floating-point type; a kernel declaring a \texttt{double} does not compile (``\texttt{'double' is not supported in Metal}''). Econometric accumulators---sums of $4\times10^5$ squared returns of magnitude $10^{-6}$, Gram matrices $X'X$ with regressors of very different scales, log-likelihoods summed over thousands of observations---lose several digits in naive FP32. The strategy has four parts.
5+
6+\paragraph{1. Double-float accumulation.} Every accumulator on the GPU is a pair $(h, \ell)$ of \texttt{float}s representing $h+\ell$ with $|\ell|\le\frac12\,\mathrm{ulp}(h)$. Additions use the error-free transformation TwoSum \citep{Dekker1971,Neumaier1974}: $s = h + v$, $e = (h - (s - (s - h))) + (v - (s - h))$, followed by a renormalisation. The pair carries about 48 significant bits; the error analysis of \citet{Higham1993} gives a bound of order $n\,u^2$ rather than $n\,u$ for a sum of $n$ terms with unit roundoff $u$. The Metal compiler must not reassociate: the kernels are compiled with \texttt{-fno-fast-math} offline and with \texttt{fastMathEnabled = NO} at runtime. Per-element inputs are still rounded to FP32 on the way in; for a sum of squares of $M$ independent returns this input rounding contributes a relative error of order $2u\sqrt{3/M}$, i.e. about $10^{-8}$ for $M = 390$, which is what we observe (Section~\ref{sec:correct}).
7+
8+\paragraph{2. Chunked partial sums combined in FP64.} A segment longer than the chunk length (4\,096 elements by default, a \texttt{PrecisionPolicy} parameter) is split into chunks; one threadgroup of 256 threads reduces each chunk with a fixed-order tree reduction of double-float values and writes a \texttt{float2}; the host adds the chunks in FP64 in a fixed order and adds the chunk-boundary cross terms ($|r_j||r_{j-1}|$ across a boundary) in FP64. The combination order is deterministic, so results are bitwise reproducible across runs for a given chunk length and threadgroup width (verified in Section~\ref{sec:correct}).
9+
10+\paragraph{3. Mixed precision by design.} Quantities that are cheap and precision-critical stay in FP64 on the CPU: the finite-sample constants, the initial variance of a GARCH recursion, the Student-$t$ density constants (Metal has no \texttt{lgamma}), the regression solve from the GPU-accumulated sufficient statistics, and all inference. The batched GARCH kernel scales each series to unit variance on the host (FP64) and un-scales the likelihood by $n\log s$, which conditions the FP32 recursion for returns of magnitude $10^{-2}$.
11+
12+\paragraph{4. Kernel fusion.} The realized kernel computes the five accumulators of \eqref{eq:rv}--\eqref{eq:rq} in a single pass over the data; the four-kernel variant exists only for the ablation. Regression statistics accumulate the upper triangle of $X'X$, $X'y$ and $y'y$ ($k\le 8$) in registers. The GARCH kernel runs one recursion per thread for one (series, parameter vector) pair---the parallelism comes from the number of parameter vectors, not from the recursion.
13+
14+\paragraph{Host side.} The backend owns one device, one command queue, one library and one pipeline cache per process; buffers come from a pool keyed by power-of-two size class in shared storage (unified memory). The shader library is loaded from a prebuilt \texttt{.metallib} when present and compiled from the embedded MSL source otherwise; both paths are measured. GPU time is read from the command buffer's \texttt{GPUStartTime}/\texttt{GPUEndTime}; wall-clock includes FP64$\to$FP32 conversion into the shared buffer, encoding, commit and completion. Metal does not expose the GPU core count; the manifest records it from \texttt{system\_profiler}.
15+
16+\paragraph{What the GPU does not do.} Optimisation drivers (BFGS line searches) run on the CPU; the GPU evaluates batches of likelihoods. Rolling sums use a direct $O(w)$-per-output kernel, which the experiments show to be slower than the CPU's $O(1)$ running sum---a limitation we report rather than hide. Nothing in the Metal path is a new estimator.
added paper/sections/microbenchmarks.tex +36 −0
@@ -0,0 +1,36 @@
1+\section{Microbenchmarks}
2+\label{sec:micro}
3+
4+\paragraph{Primitives on the laptop.} Table~\ref{tab:prim} (abridged; the full grid is in the repository) and Figure~\ref{fig:prim} report the fused realized kernel, regression sufficient statistics ($k = 4$), the rolling sum of squares ($w = 22$) and the batched GARCH negative log-likelihood with one ($P = 1$) and 64 ($P = 64$) parameter vectors per series, for series counts $S \in \{1, 10, 100, 1\,000, 10\,000\}$ and lengths $n \in \{390, 3\,900, 39\,000, 390\,000\}$ of Student-$t(5)$ synthetic returns.
5+
6+\begin{table}[t]
7+\centering\scriptsize
8+\caption{Primitive kernels, laptop (M5 Max). Medians of wall-clock milliseconds over 5 repetitions; CPU MT uses 18 threads; speedup $= T_{\text{CPU MT}}/T_{\text{Metal}}$; verdict by non-overlapping bootstrap CIs.}
9+\label{tab:prim}
10+\input{tables/tab_primitives}
11+\end{table}
12+
13+\begin{figure}[t]
14+\centering\includegraphics[width=\textwidth]{figures/fig_primitives_throughput.pdf}
15+\caption{Throughput of the primitives (elements per second, medians) versus problem size on the laptop.}
16+\label{fig:prim}
17+\end{figure}
18+
19+Four findings answer RQ1 at the kernel level. First, the fused realized kernel is faster than the 18-thread CPU from about $4\times10^4$ elements upward ($100\times390$: 2.4$\times$) and reaches 2.0--3.4$\times$ between $3.9\times10^6$ and $3.9\times10^7$ elements; a few shapes at $3.9\times10^5$ elements are ties. Second, regression statistics break even only around $4\times10^5$ elements for a single long series (3.9$\times$), are ties for most shapes between $4\times10^5$ and $4\times10^6$, and gain 1.1--1.4$\times$ at $4\times10^7$: the per-row cost on the CPU is already small and the GPU pays conversion and launch overhead. Third, the rolling sum of squares is a negative result: the direct $O(w)$ kernel never beats the CPU's $O(1)$ running sum (0.4--0.9$\times$ of CPU MT at every size). Fourth, batched GARCH likelihoods are the clearest GPU case, but only when many parameter vectors are evaluated per series: with $P = 1$ the CPU wins everywhere (the GPU is launch-bound and the recursion is sequential), with $P = 64$ Metal wins from 100 series (1.9$\times$) up to 7.9$\times$ at $1\,000\times3\,900$. For the reductions, device time is 5--20 times smaller than wall-clock: FP64$\to$FP32 conversion and buffer fills, not the kernels, set the wall-clock. Unified memory removes the copy to the device but not the conversion.
20+
21+\paragraph{Shader compilation and library loading.} Loading the prebuilt library takes 0.2\,ms and the first pipeline creation 14\,ms on the laptop; compiling the embedded MSL source at runtime takes 39\,ms when the system shader cache is warm and 123\,ms cold (compiler service start included). On cluster nodes the cold compile is 37--49\,ms (110\,ms on the 8\,GB M2 mini) and the warm compile 0.3--0.6\,ms (Table~\ref{tab:e7}). These costs are paid once per process and matter only for workloads of a few milliseconds.
22+
23+\paragraph{Generation sweep (E7).} Table~\ref{tab:e7} and Figure~\ref{fig:e7} report the same bundle on eleven machines. All Metal tests pass on every machine with both the prebuilt library and runtime compilation, including the 8\,GB M2 mini without developer tools. The speedup of the fused realized kernel at $1\,000\times3\,900$ ranges from 4.0$\times$ (M2, 10 GPU cores, 8 CPU cores) and 3.6$\times$ (M2~Max) down to 1.1$\times$ on the M5~Max and M5~Pro; GARCH $P = 64$ from 7.8$\times$ (M2~Max) to 1.8$\times$ (M5~Pro); the rolling kernel loses everywhere (0.2--0.65$\times$). The pattern is not ``more GPU cores, more speedup'': the ratio is governed by the CPU's multicore throughput, which grew faster across generations than the GPU throughput of these memory-bound kernels, and by the conversion overhead, which is CPU-bound. The GPU helps most on the machines with the weakest CPUs.
24+
25+\begin{table}[t]
26+\centering\scriptsize
27+\caption{E7 --- generation sweep (3 repetitions, shuffled; speedups are CPU MT / Metal medians at the stated shape; tests = Metal test suite with the prebuilt metallib / with runtime MSL compilation).}
28+\label{tab:e7}
29+\input{tables/tab_e7}
30+\end{table}
31+
32+\begin{figure}[t]
33+\centering\includegraphics[width=.8\textwidth]{figures/fig_e7.pdf}
34+\caption{E7 --- Metal/CPU-MT speedup versus GPU core count for three kernels on eleven Apple Silicon machines.}
35+\label{fig:e7}
36+\end{figure}
added paper/sections/reproducibility.tex +8 −0
@@ -0,0 +1,8 @@
1+\section{Reproducibility and licensing}
2+\label{sec:repro}
3+
4+The repository (\texttt{wp40\_raid}, git remote on the author's SPB Git server) contains the library, kernels, bindings, CLI, tests, benchmark drivers, every raw benchmark record (\texttt{experiments/results/raw/*.jsonl}) with its manifest (host, OS, compiler, library versions, load, thermal state, seeds, input hashes), the scripts that generate every table and figure of this paper from those records (\texttt{experiments/scripts/make\_paper\_assets.py}; the SHA-256 of each input and output is written to \texttt{experiments/manifests/paper\_assets.json}), and \texttt{RESEARCH\_STATUS.md}, which lists what is implemented, verified, experimental and deliberately not implemented. No number in the text was typed by hand: the prose quotes \texttt{experiments/results/key\_numbers.json}.
5+
6+A clean checkout builds with CMake presets (\texttt{macos-release} with Metal and Arrow; \texttt{cpu-only} for Linux) and passes 55 test cases; the Python package builds with scikit-build-core. The synthetic fixtures and the reference values produced by \texttt{statsmodels}, \texttt{arch}, \texttt{exchange\_calendars}, \texttt{highfrequency} and \texttt{rugarch} are committed, so the validation suite runs without network access or vendor data. Reproducing E1--E6 requires an API key for the FinData~OS lakehouse; the universe selection is seeded and the dataset version is pinned, so the same bars are served to a reader with access. E7 requires only the bundle and SSH access to the machines.
7+
8+\raid{} is released under the MIT licence; Eigen (MPL-2.0), Apache Arrow (Apache-2.0), nanobind (BSD-3) and Catch2 (BSL-1.0) are its dependencies. Apple Metal and Accelerate are proprietary system frameworks; the Metal backend is optional and the CPU backend is portable. FirstRate Data bars are used under a private licence and are not redistributed; the paper reports aggregates only, and we thank FirstRate Data for the data.
added paper/sections/setup.tex +12 −0
@@ -0,0 +1,12 @@
1+\section{Experimental setup}
2+\label{sec:setup}
3+
4+\paragraph{Data.} All market data come from FinData~OS, the author's private lakehouse (\url{https://findata.spboucher.ai/api/v1}) built on FirstRate Data US equity bars under a private-use licence; raw bars are not redistributed and only aggregates appear in this paper. Bars are 1-minute OHLCV, stamped at the bar \emph{open} in UTC, with the extended session 04:00--20:00~ET and the regular session 09:30--16:00~ET (the 16:00 bar is the closing print); zero-volume minutes are absent at the source. The universe (experiment E0) is a seeded random sample of 1\,000 symbols among the 6\,046 of the \texttt{market\_bars.stock.1m.raw} dataset that were active through all of 2024 (first session on or before 2024-01-02, last on or after 2024-12-31, delisted suffixes excluded); nested sub-samples of 1, 10 and 100 symbols are its first elements. Twenty asynchronous Parquet exports (50 symbols each, 43.3 million bars, 0.63\,GB, dataset version 17\,375, 437\,s including polling) are cached with their SHA-256 and request identifiers. Of the 1\,000 symbols, 888 have at least 200 regular sessions with positive realized variance and 926 at least 200 sessions of returns; the experiments report the counts they use. For E3 we add ten years of daily bars (2015--2024, 1.82 million rows) for the same symbols. All experiment inputs are listed with hashes in \texttt{experiments/manifests/}.
5+
6+The provider's own conventions were verified live before use (opt-in integration tests): the per-session expected bar counts (391; 211 on early closes) equal \raid{}'s calendar for every session queried; page downloads equal exports bar for bar; and \raid{}'s calendar-aware 1-minute to 5-minute aggregation in extended-session mode reproduces the provider's derived 5-minute dataset exactly (280 of 280 regular-window bars, OHLCV), while in regular-session mode the only difference is the closing bin, where the provider's 16:00 bar also contains the 16:01--16:04 post-close minutes.
7+
8+\paragraph{Hardware and software.} The development and benchmark machine is an Apple M5~Max laptop: 18 CPU cores, 40 GPU cores, 48\,GB unified memory, macOS~27.0, Xcode~27.2, Apple clang~21, Metal~4 (GPU families Apple1--Apple10). Dependencies: Eigen~5.0.1 with Accelerate BLAS/LAPACK, Apache Arrow/Parquet~26.0.0, Catch2~3.16.1, nanobind~3.1.0, Python~3.12 with NumPy~2.5.4, pandas, Polars~2.1.0, DuckDB~2.0.0, statsmodels~0.15.0, arch~8.0.0, SciPy~1.18.1; R~4.6.1 with \texttt{highfrequency}~1.0.3 and \texttt{rugarch}~1.5.6. For E7, eleven other Apple Silicon machines of the author's cluster (M2 to M6, 8 to 96\,GB, 10 to 60 GPU cores) run a self-contained bundle built with \texttt{-mcpu=apple-m1} and a macOS~26 deployment target; nothing is installed on them, and the hosts that serve production services were excluded.
9+
10+\paragraph{Protocol.} Each benchmark case is run twice as warm-up and then $R$ times ($R = 5$ on the laptop, 3 on cluster nodes); case order is shuffled with a fixed seed; the reported statistic is the median of wall-clock seconds with a 2\,000-draw bootstrap 95\,\% confidence interval of the median. A backend ``wins'' when the intervals do not overlap; otherwise the case is a tie. Wall-clock for Metal includes the FP64$\to$FP32 conversion into shared buffers, encoding, commit and completion; GPU device time is reported separately. Shader compilation is measured apart (cold, cache-busted with a salted source; warm; and prebuilt library). Each run writes JSON records and a manifest with host, OS, compiler, library versions, load average, thermal state, seed and input hashes; peak resident set size is read from \texttt{getrusage}. The laptop was shared with other sessions during the runs (load average 2.5--4 on 18 cores), which the manifests record; cluster nodes were idle. Energy was not measured (no instrument), and we make no energy claim.
11+
12+\paragraph{Experiments.} E1: batched realized measures versus panel width and segment length. E2: HAR rolling forecasts and batched estimation. E3: GARCH-family panel estimation and a GPU likelihood grid. E4: realized covariance and rolling connectedness. E5: Monte Carlo inference and reproducibility (Section~\ref{sec:correct}). E6: the Arrow/Parquet-to-estimator pipeline against pandas, Polars and DuckDB. E7: the generation sweep. Micro-benchmarks of the seven primitives precede them.
added paper/sections/threats.tex +12 −0
@@ -0,0 +1,12 @@
1+\section{Threats to validity}
2+\label{sec:threats}
3+
4+\paragraph{Construct.} Speedups compare \raid{}'s own CPU backend with its own Metal backend; the CPU backend is a careful but not a vendor-tuned implementation (Accelerate is used for linear algebra, not for the reductions). A hand-vectorised AVX/NEON CPU kernel could narrow the gaps further. Conversely, the GPU kernels are first implementations: the rolling kernel in particular is known to be the wrong algorithm.
5+
6+\paragraph{Internal.} The laptop was shared with other processes during the runs (load average 2.5--4 on 18 cores); medians with bootstrap intervals and shuffled order mitigate but do not remove this, and the cluster nodes, which were idle, show the same qualitative pattern. Thermal state was recorded (no throttling warnings) but power mode was not controlled. The real panel is one year of one market; the synthetic panels have Student-$t(5)$ returns with no intraday seasonality.
7+
8+\paragraph{Statistical.} The forecasting results of E2 and E3 use short windows and a fixed-parameter evaluation and are illustrations of equivalence, not model comparisons; no model confidence set or Diebold--Mariano test is reported, and none should be inferred. The provider's point-in-time guarantees apply to fundamentals, which this paper does not use; corporate actions are irrelevant to within-session returns but the close-to-close daily returns of E3 are from unadjusted bars and contain split days as outliers (the QML fits are robust to a handful of such days, but we did not remove them).
9+
10+\paragraph{External.} All data are US equities from a single vendor; all hardware is Apple Silicon. The conclusions about FP32 accumulation transfer to other FP32-only accelerators in principle, the timing conclusions do not.
11+
12+\paragraph{Scope.} Nothing here is a microstructure result: 1-minute bars cannot support noise-robust estimators or order-flow measures, and the registry marks them as not implemented. Energy was not measured.
added paper/tables/tab_e1.tex +25 −0
@@ -0,0 +1,25 @@
1+\begin{tabular}{llrrrrrrl}
2+\toprule
3+Panel & $S$ & segment & elements & CPU 1T (ms) & CPU MT (ms) & Metal fused (ms) & Metal 4 kernels (ms) & max rel.\ err.\ RV \\
4+\midrule
5+real & 1 & day & 88,350 & 0.77 & 0.94 & 0.94 & 2.27 & 3.2e-08 \\
6+real & 1 & week & 87,732 & 0.81 & 0.91 & 0.77 & 1.99 & 1.5e-08 \\
7+real & 1 & month & 85,202 & 0.59 & 0.26 & 0.33 & 1.03 & 5.5e-09 \\
8+real & 1 & year & 88,350 & 0.88 & 0.74 & 0.51 & 1.29 & 1.2e-09 \\
9+real & 10 & day & 660,818 & 4.59 & 1.33 & 1.40 & 1.95 & 1.2e-07 \\
10+real & 10 & week & 655,478 & 4.32 & 1.02 & 0.81 & 1.97 & 4.3e-08 \\
11+real & 10 & month & 634,608 & 4.16 & 0.90 & 1.06 & 1.49 & 1.5e-08 \\
12+real & 10 & year & 660,818 & 4.49 & 1.31 & 1.41 & 2.38 & 3.2e-09 \\
13+real & 100 & day & 4,857,139 & 29.41 & 3.80 & 4.11 & 7.73 & 1.5e-07 \\
14+real & 100 & week & 4,814,500 & 28.38 & 3.75 & 3.63 & 6.06 & 1.4e-07 \\
15+real & 100 & month & 4,656,568 & 27.86 & 3.61 & 3.43 & 5.32 & 6.5e-08 \\
16+real & 100 & year & 4,817,403 & 29.64 & 3.89 & 3.25 & 5.53 & 1.2e-08 \\
17+real & 926 & day & 40,408,948 & 240.89 & 29.77 & 27.77 & 51.85 & 1.6e-07 \\
18+real & 926 & week & 40,062,893 & 235.07 & 29.19 & 24.93 & 44.04 & 1.4e-07 \\
19+real & 926 & month & 38,769,255 & 231.20 & 28.67 & 20.43 & 38.21 & 6.5e-08 \\
20+real & 926 & year & 39,917,613 & 241.63 & 28.71 & 20.41 & 39.65 & 1.4e-08 \\
21+synthetic & 10000 & day & 3,900,000 & 21.74 & 2.68 & 3.35 & -- & 4.9e-08 \\
22+synthetic & 10000 & week & 19,500,000 & 106.64 & 12.07 & 9.12 & -- & 3.4e-08 \\
23+synthetic & 10000 & month & 85,800,000 & 483.17 & 54.08 & 35.41 & -- & 2.8e-08 \\
24+\bottomrule
25+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e2_accuracy.tex +9 −0
@@ -0,0 +1,9 @@
1+\begin{tabular}{lrrrrrr}
2+\toprule
3+$h$ & symbols & HAR QLIKE & log-HAR QLIKE & HARQ QLIKE & RW QLIKE & HAR beats RW (\%) \\
4+\midrule
5+1 & 100 & 0.196 & 0.205 & 0.203 & 0.251 & 84.0 \\
6+5 & 100 & 0.146 & 0.138 & 0.150 & 0.239 & 86.0 \\
7+22 & 100 & 0.134 & 0.112 & 0.132 & 0.301 & 78.0 \\
8+\bottomrule
9+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e2_runtime.tex +9 −0
@@ -0,0 +1,9 @@
1+\begin{tabular}{rrrrrr}
2+\toprule
3+$S$ & $T$ & per-series 1T (ms) & per-series 18T (ms) & batched CPU (ms) & batched Metal (ms) \\
4+\midrule
5+10 & 252 & 0.077 & 0.053 & 0.102 & 0.320 \\
6+100 & 252 & 0.765 & 0.145 & 0.651 & 0.753 \\
7+781 & 251 & 6.034 & 0.684 & 3.861 & 4.703 \\
8+\bottomrule
9+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e3_fits.tex +10 −0
@@ -0,0 +1,10 @@
1+\begin{tabular}{lrrrrrrr}
2+\toprule
3+Model & fits & converged (\%) & median iterations & median time (ms) & median persistence & QLIKE vs RV & QLIKE RW \\
4+\midrule
5+garch.n & 888 & 100.0 & 14 & 2.86 & 0.962 & 0.403 & 0.276 \\
6+gjr.n & 888 & 98.4 & 18 & 4.00 & 0.971 & 0.399 & 0.273 \\
7+egarch.n & 888 & 92.8 & 17 & 18.15 & 0.956 & 0.396 & 0.272 \\
8+garch.t & 888 & 100.0 & 18 & 6.75 & 0.973 & 0.391 & 0.276 \\
9+\bottomrule
10+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e4.tex +10 −0
@@ -0,0 +1,10 @@
1+\begin{tabular}{rrrrrrrr}
2+\toprule
3+$N$ & days used & cond.\ median & cond.\ max & LW shrinkage & DY windows (120 d) & mean total (\%) & time (ms) \\
4+\midrule
5+10 & 182 & 81 & 1114 & 0.16 & 133 & 37.2 & 0.5 \\
6+30 & 129 & 422 & 1982 & 0.26 & 133 & 69.7 & 2.3 \\
7+60 & 95 & 5918 & 4004356648938841745923394298009895722138797464276046077972691059680375614967856131465364276842948260023900075942269994798777250243494084095054077702593831762122007941088576324164555345119533344107104725633883477071585852614164068406989272550433554121982002137825411034442526924333102027514438483968 & 0.33 & 133 & 87.5 & 8.9 \\
8+100 & 88 & 13073204915206876047919794817195177267780192312449123749575822851529740987667064655074455547348290915018643457594152267517165690738414825634522433173317554354471431035532992099674255880672675625566882041983402524828121189704828945884183280239005287033607779588484361348884205850359248967567652421632 & 46288488850355617690769830052849592093140761725393381476083077218419594779461118881406549817489952414039431887036530303721787054620313140438309366826725501885618978872251636841509737271391224828467022348517545939208321065913241119279635028191304586347859300448274891042774110350484075336561221173248 & 0.33 & 133 & 97.2 & 28.8 \\
9+\bottomrule
10+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e5.tex +16 −0
@@ -0,0 +1,16 @@
1+\begin{tabular}{llrrrr}
2+\toprule
3+DGP & parameter & bias & RMSE & coverage 95\% & converged \\
4+\midrule
5+GARCH(1,1) $n=500$ & $\mu$ & -0.0005 & 0.0383 & 0.950 & 1000/1000 \\
6+GARCH(1,1) $n=500$ & $\omega$ & +0.0201 & 0.0487 & 0.954 & 1000/1000 \\
7+GARCH(1,1) $n=500$ & $\alpha$ & +0.0021 & 0.0304 & 0.896 & 1000/1000 \\
8+GARCH(1,1) $n=500$ & $\beta$ & -0.0256 & 0.0709 & 0.916 & 1000/1000 \\
9+GARCH(1,1) $n=2000$ & $\mu$ & +0.0001 & 0.0190 & 0.961 & 1000/1000 \\
10+GARCH(1,1) $n=2000$ & $\omega$ & +0.0034 & 0.0093 & 0.972 & 1000/1000 \\
11+GARCH(1,1) $n=2000$ & $\alpha$ & +0.0007 & 0.0137 & 0.932 & 1000/1000 \\
12+GARCH(1,1) $n=2000$ & $\beta$ & -0.0048 & 0.0196 & 0.936 & 1000/1000 \\
13+HAR $h=1$ (HAC) & $\beta_0,\beta_d,\beta_w,\beta_m$ & -- & -- & 0.945, 0.939, 0.946, 0.941 & 1000/1000 \\
14+HAR $h=5$ (HAC) & $\beta_0,\beta_d,\beta_w,\beta_m$ & -- & -- & 0.909, 0.936, 0.931, 0.900 & 1000/1000 \\
15+\bottomrule
16+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e6.tex +11 −0
@@ -0,0 +1,11 @@
1+\begin{tabular}{lrrrl}
2+\toprule
3+Engine & median total (s) & 95\% CI & bars / s & notes \\
4+\midrule
5+RAID cpu & 0.118 & [0.097, 0.128] & 20.0M & Arrow projection, exact calendar, 5 measures \\
6+RAID metal & 0.125 & [0.101, 0.134] & 19.0M & same, fused Metal kernel \\
7+polars & 0.165 & [0.146, 0.172] & 14.4M & approximate early-close rule, lazy scan \\
8+duckdb & 0.164 & [0.158, 0.165] & 14.5M & approximate early-close rule, SQL window \\
9+pandas & 2.558 & [2.355, 2.634] & 0.9M & approximate early-close rule, groupby.apply \\
10+\bottomrule
11+\end{tabular}
\ No newline at end of file
added paper/tables/tab_e7.tex +17 −0
@@ -0,0 +1,17 @@
1+\begin{tabular}{llrrrllrrrr}
2+\toprule
3+Node & chip & GPU cores & CPU thr. & GB & tests (metallib / runtime) & cold / warm compile (ms) & RV fused $1000\times3900$ speedup & regression speedup & GARCH P=64 speedup & rolling speedup \\
4+\midrule
5+m2m8a & Apple M2 & 10 & 8 & 8 & pass / pass & 110 / 1 & 3.99 & 1.61 & 6.67 & 0.65 \\
6+M2M32c & Apple M2 Max & 30 & 12 & 32 & pass / pass & 48 / 1 & 3.61 & 1.35 & 7.84 & 0.52 \\
7+M2U64 & Apple M2 Ultra & 60 & 24 & 64 & pass / pass & 49 / 1 & 2.18 & 1.08 & 4.92 & 0.32 \\
8+M3BA24 & Apple M3 & 10 & 8 & 24 & pass / pass & 44 / 0 & 2.69 & 1.68 & 6.21 & 0.36 \\
9+M3U96b & Apple M3 Ultra & 60 & 28 & 96 & pass / pass & 47 / 1 & 1.68 & 0.78 & 3.18 & 0.24 \\
10+m4md & Apple M4 & 10 & 10 & 16 & pass / pass & 42 / 0 & 2.55 & 1.34 & 4.21 & 0.38 \\
11+m4mm & Apple M4 Pro & 16 & 12 & 48 & pass / pass & 39 / 0 & 1.63 & 0.91 & 2.14 & 0.24 \\
12+M4M64c & Apple M4 Max & 40 & 16 & 64 & pass / pass & 40 / 0 & 2.80 & 1.19 & 4.82 & 0.48 \\
13+m5ma & Apple M5 Pro & 16 & 15 & 24 & pass / pass & 44 / 0 & 1.13 & 0.55 & 1.83 & 0.20 \\
14+M5M36 & Apple M5 Max & 32 & 18 & 36 & pass / pass & 37 / 0 & 1.12 & 0.60 & 2.14 & 0.23 \\
15+m6ma & Apple M6 & 12 & 12 & 24 & pass / pass & 40 / 0 & 1.63 & 0.79 & 3.72 & 0.34 \\
16+\bottomrule
17+\end{tabular}
\ No newline at end of file
added paper/tables/tab_primitives.tex +75 −0
@@ -0,0 +1,75 @@
1+\begin{tabular}{llrrrrrl}
2+\toprule
3+Kernel & $S\times n$ & elements & CPU 1T (ms) & CPU MT (ms) & Metal (ms) & speedup & verdict \\
4+\midrule
5+realized\_fused & $1\times390$ & 390 & 0.001 & 0.001 & 0.234 & 0.01 & CPU \\
6+realized\_fused & $1\times3900$ & 3,900 & 0.012 & 0.011 & 0.633 & 0.02 & CPU \\
7+realized\_fused & $10\times390$ & 3,900 & 0.011 & 0.142 & 0.233 & 0.61 & CPU \\
8+realized\_fused & $1\times39000$ & 39,000 & 0.183 & 0.167 & 0.207 & 0.81 & CPU \\
9+realized\_fused & $10\times3900$ & 39,000 & 0.188 & 0.135 & 0.164 & 0.82 & CPU \\
10+realized\_fused & $100\times390$ & 39,000 & 0.187 & 0.552 & 0.226 & 2.44 & Metal \\
11+realized\_fused & $1\times390000$ & 390,000 & 2.032 & 2.086 & 0.935 & 2.23 & Metal \\
12+realized\_fused & $10\times39000$ & 390,000 & 2.573 & 0.363 & 0.718 & 0.51 & tie \\
13+realized\_fused & $100\times3900$ & 390,000 & 2.043 & 0.602 & 0.641 & 0.94 & tie \\
14+realized\_fused & $1000\times390$ & 390,000 & 2.236 & 0.840 & 0.338 & 2.48 & Metal \\
15+realized\_fused & $10\times390000$ & 3,900,000 & 20.892 & 2.527 & 1.022 & 2.47 & Metal \\
16+realized\_fused & $100\times39000$ & 3,900,000 & 20.591 & 2.190 & 1.169 & 1.87 & Metal \\
17+realized\_fused & $1000\times3900$ & 3,900,000 & 21.245 & 1.742 & 0.875 & 1.99 & Metal \\
18+realized\_fused & $10000\times390$ & 3,900,000 & 21.238 & 2.126 & 2.213 & 0.96 & tie \\
19+realized\_fused & $100\times390000$ & 39,000,000 & 232.587 & 18.793 & 8.720 & 2.16 & Metal \\
20+realized\_fused & $1000\times39000$ & 39,000,000 & 238.772 & 20.247 & 5.892 & 3.44 & Metal \\
21+realized\_fused & $10000\times3900$ & 39,000,000 & 233.672 & 15.694 & 7.803 & 2.01 & Metal \\
22+regression\_stats & $1\times390$ & 390 & 0.003 & 0.003 & 0.894 & 0.00 & CPU \\
23+regression\_stats & $1\times3900$ & 3,900 & 0.030 & 0.030 & 0.348 & 0.09 & CPU \\
24+regression\_stats & $10\times390$ & 3,900 & 0.031 & 0.041 & 0.652 & 0.06 & CPU \\
25+regression\_stats & $1\times39000$ & 39,000 & 0.302 & 0.384 & 0.409 & 0.94 & tie \\
26+regression\_stats & $10\times3900$ & 39,000 & 0.300 & 0.076 & 0.362 & 0.21 & CPU \\
27+regression\_stats & $100\times390$ & 39,000 & 0.361 & 0.176 & 0.336 & 0.52 & CPU \\
28+regression\_stats & $1\times390000$ & 390,000 & 2.973 & 3.043 & 0.790 & 3.85 & Metal \\
29+regression\_stats & $10\times39000$ & 390,000 & 3.084 & 0.562 & 0.696 & 0.81 & CPU \\
30+regression\_stats & $100\times3900$ & 390,000 & 2.998 & 0.440 & 0.595 & 0.74 & CPU \\
31+regression\_stats & $1000\times390$ & 390,000 & 3.121 & 0.697 & 0.581 & 1.20 & tie \\
32+regression\_stats & $10\times390000$ & 3,900,000 & 32.663 & 6.655 & 4.117 & 1.62 & Metal \\
33+regression\_stats & $100\times39000$ & 3,900,000 & 32.191 & 3.174 & 4.469 & 0.71 & tie \\
34+regression\_stats & $1000\times3900$ & 3,900,000 & 34.396 & 3.422 & 3.796 & 0.90 & tie \\
35+regression\_stats & $10000\times390$ & 3,900,000 & 30.644 & 3.959 & 4.191 & 0.94 & tie \\
36+regression\_stats & $100\times390000$ & 39,000,000 & 301.369 & 33.760 & 30.709 & 1.10 & Metal \\
37+regression\_stats & $1000\times39000$ & 39,000,000 & 324.036 & 31.241 & 26.743 & 1.17 & Metal \\
38+regression\_stats & $10000\times3900$ & 39,000,000 & 356.914 & 37.490 & 26.574 & 1.41 & Metal \\
39+rolling\_sumsq & $1\times390$ & 390 & 0.001 & 0.001 & 0.284 & 0.00 & CPU \\
40+rolling\_sumsq & $1\times3900$ & 3,900 & 0.007 & 0.007 & 0.242 & 0.03 & CPU \\
41+rolling\_sumsq & $10\times390$ & 3,900 & 0.007 & 0.018 & 0.203 & 0.09 & CPU \\
42+rolling\_sumsq & $1\times39000$ & 39,000 & 0.069 & 0.070 & 0.165 & 0.43 & CPU \\
43+rolling\_sumsq & $10\times3900$ & 39,000 & 0.069 & 0.045 & 0.161 & 0.28 & CPU \\
44+rolling\_sumsq & $100\times390$ & 39,000 & 0.070 & 0.180 & 0.268 & 0.67 & tie \\
45+rolling\_sumsq & $1\times390000$ & 390,000 & 0.691 & 0.719 & 0.790 & 0.91 & tie \\
46+rolling\_sumsq & $10\times39000$ & 390,000 & 0.710 & 0.133 & 0.736 & 0.18 & CPU \\
47+rolling\_sumsq & $100\times3900$ & 390,000 & 0.807 & 0.203 & 0.682 & 0.30 & CPU \\
48+rolling\_sumsq & $1000\times390$ & 390,000 & 0.719 & 0.271 & 0.908 & 0.30 & CPU \\
49+rolling\_sumsq & $10\times390000$ & 3,900,000 & 6.914 & 1.766 & 2.544 & 0.69 & CPU \\
50+rolling\_sumsq & $100\times39000$ & 3,900,000 & 7.338 & 1.087 & 2.227 & 0.49 & CPU \\
51+rolling\_sumsq & $1000\times3900$ & 3,900,000 & 6.954 & 1.027 & 2.145 & 0.48 & CPU \\
52+rolling\_sumsq & $10000\times390$ & 3,900,000 & 7.048 & 1.072 & 2.163 & 0.50 & CPU \\
53+rolling\_sumsq & $100\times390000$ & 39,000,000 & 69.622 & 9.413 & 14.852 & 0.63 & CPU \\
54+rolling\_sumsq & $1000\times39000$ & 39,000,000 & 70.263 & 8.312 & 14.729 & 0.56 & CPU \\
55+rolling\_sumsq & $10000\times3900$ & 39,000,000 & 69.226 & 6.543 & 14.753 & 0.44 & CPU \\
56+garch\_nll\_P1 & $1\times390$ & 390 & 0.002 & 0.002 & 0.918 & 0.00 & CPU \\
57+garch\_nll\_P1 & $1\times3900$ & 3,900 & 0.016 & 0.017 & 1.349 & 0.01 & CPU \\
58+garch\_nll\_P1 & $10\times390$ & 3,900 & 0.022 & 0.029 & 0.343 & 0.09 & CPU \\
59+garch\_nll\_P1 & $10\times3900$ & 39,000 & 0.216 & 0.069 & 1.430 & 0.05 & CPU \\
60+garch\_nll\_P1 & $100\times390$ & 39,000 & 0.192 & 0.161 & 0.624 & 0.26 & CPU \\
61+garch\_nll\_P1 & $100\times3900$ & 390,000 & 1.615 & 0.207 & 1.649 & 0.13 & CPU \\
62+garch\_nll\_P1 & $1000\times390$ & 390,000 & 1.639 & 0.277 & 1.153 & 0.24 & CPU \\
63+garch\_nll\_P1 & $1000\times3900$ & 3,900,000 & 18.310 & 1.498 & 7.207 & 0.21 & CPU \\
64+garch\_nll\_P1 & $10000\times390$ & 3,900,000 & 17.741 & 2.243 & 6.755 & 0.33 & CPU \\
65+garch\_nll\_P1 & $10000\times3900$ & 39,000,000 & 162.264 & 13.833 & 61.066 & 0.23 & CPU \\
66+garch\_nll\_P64 & $1\times390$ & 390 & 0.083 & 0.172 & 0.501 & 0.34 & CPU \\
67+garch\_nll\_P64 & $1\times3900$ & 3,900 & 0.808 & 0.142 & 1.424 & 0.10 & CPU \\
68+garch\_nll\_P64 & $10\times390$ & 3,900 & 0.826 & 0.244 & 1.018 & 0.24 & CPU \\
69+garch\_nll\_P64 & $10\times3900$ & 39,000 & 8.084 & 0.776 & 1.382 & 0.56 & CPU \\
70+garch\_nll\_P64 & $100\times390$ & 39,000 & 8.347 & 1.006 & 0.533 & 1.89 & Metal \\
71+garch\_nll\_P64 & $100\times3900$ & 390,000 & 80.134 & 6.174 & 2.170 & 2.84 & Metal \\
72+garch\_nll\_P64 & $1000\times390$ & 390,000 & 96.799 & 8.487 & 2.337 & 3.63 & Metal \\
73+garch\_nll\_P64 & $1000\times3900$ & 3,900,000 & 856.179 & 81.506 & 10.273 & 7.93 & Metal \\
74+\bottomrule
75+\end{tabular}
\ No newline at end of file
modified src/estimators/realized.cpp +13 −7
@@ -1,6 +1,8 @@
1 #include "raid/estimators/realized.hpp" 1 #include "raid/estimators/realized.hpp"
2 #include "raid/core/error.hpp" 2 #include "raid/core/error.hpp"
3 #include "raid/statistics/reductions.hpp" 3 #include "raid/statistics/reductions.hpp"
4+#include "raid/core/thread_pool.hpp"
5+#include <mutex>
4 #include <cmath> 6 #include <cmath>
5 #include <numbers> 7 #include <numbers>
6 8
@@ -25,7 +27,15 @@ RealizedMeasures realized_measures_batch(const backend::Batch& b, const Realized
25 m.stats.device_seconds = st1.device_seconds + st2.device_seconds + st3.device_seconds + st4.device_seconds; m.stats.note = reason + (m.stats.note.empty() ? "" : " | " + m.stats.note); 27 m.stats.device_seconds = st1.device_seconds + st2.device_seconds + st3.device_seconds + st4.device_seconds; m.stats.note = reason + (m.stats.note.empty() ? "" : " | " + m.stats.note);
26 m.bpv.resize(S); m.rq.resize(S); m.jump.resize(S); m.tripv.resize(S); m.n_returns.resize(S); m.daily_return.resize(S); 28 m.bpv.resize(S); m.rq.resize(S); m.jump.resize(S); m.tripv.resize(S); m.n_returns.resize(S); m.daily_return.resize(S);
27 const f64 mu43 = mu_p(4.0 / 3.0); 29 const f64 mu43 = mu_p(4.0 / 3.0);
28− for (usize s = 0; s < S; ++s) { 30+ if (opt.tripower) {
31+ ThreadPool::global().parallel_for(S, [&](usize s0, usize s1) { std::vector<f64> p43; for (usize s = s0; s < s1; ++s) { const usize M = b.size(s); auto x = b.series(s);
32+ if (M <= 2) { m.tripv[s] = std::numeric_limits<f64>::quiet_NaN(); continue; }
33+ p43.resize(M); for (usize j = 0; j < M; ++j) { const f64 a = std::abs(x[j]); p43[j] = std::cbrt(a * a * a * a); } // |x|^{4/3} = cbrt(x⁴)
34+ f64 acc = 0, c = 0; for (usize j = 2; j < M; ++j) { const f64 v = p43[j] * p43[j - 1] * p43[j - 2], t = acc + v; if (std::abs(acc) >= v) c += (acc - t) + v; else c += (v - t) + acc; acc = t; }
35+ const f64 fs3 = opt.bpv_finite_sample ? static_cast<f64>(M) / static_cast<f64>(M - 2) : 1.0; m.tripv[s] = fs3 * (acc + c) / (mu43 * mu43 * mu43); } });
36+ } else m.tripv.assign(S, std::numeric_limits<f64>::quiet_NaN());
37+ std::mutex warn_m;
38+ ThreadPool::global().parallel_for(S, [&](usize s0, usize s1) { for (usize s = s0; s < s1; ++s) {
29 const usize M = b.size(s); m.n_returns[s] = M; 39 const usize M = b.size(s); m.n_returns[s] = M;
30 auto x = b.series(s); 40 auto x = b.series(s);
31 m.daily_return[s] = stats::sum_neumaier(x); 41 m.daily_return[s] = stats::sum_neumaier(x);
@@ -33,13 +43,9 @@ RealizedMeasures realized_measures_batch(const backend::Batch& b, const Realized
33 m.bpv[s] = M > 1 ? (1.0 / (kMu1 * kMu1)) * fs * adj[s] : std::numeric_limits<f64>::quiet_NaN(); 43 m.bpv[s] = M > 1 ? (1.0 / (kMu1 * kMu1)) * fs * adj[s] : std::numeric_limits<f64>::quiet_NaN();
34 m.rq[s] = M > 0 ? (static_cast<f64>(M) / 3.0) * s4[s] : std::numeric_limits<f64>::quiet_NaN(); 44 m.rq[s] = M > 0 ? (static_cast<f64>(M) / 3.0) * s4[s] : std::numeric_limits<f64>::quiet_NaN();
35 m.jump[s] = std::max(m.rv[s] - m.bpv[s], 0.0); 45 m.jump[s] = std::max(m.rv[s] - m.bpv[s], 0.0);
36− // tripower (CPU, FP64 — small cost, not batched on GPU in Tier A) 46+ if (M < 2) { std::lock_guard<std::mutex> lk(warn_m); m.warnings.push_back("segment " + std::to_string(s) + ": fewer than 2 returns"); }
37− if (M > 2) { f64 acc = 0, c = 0; for (usize j = 2; j < M; ++j) { const f64 v = std::pow(std::abs(x[j]), 4.0 / 3.0) * std::pow(std::abs(x[j - 1]), 4.0 / 3.0) * std::pow(std::abs(x[j - 2]), 4.0 / 3.0), t = acc + v; if (std::abs(acc) >= v) c += (acc - t) + v; else c += (v - t) + acc; acc = t; }
38− const f64 fs3 = opt.bpv_finite_sample ? static_cast<f64>(M) / static_cast<f64>(M - 2) : 1.0; m.tripv[s] = fs3 * (acc + c) / (mu43 * mu43 * mu43); }
39− else m.tripv[s] = std::numeric_limits<f64>::quiet_NaN();
40− if (M < 2) m.warnings.push_back("segment " + std::to_string(s) + ": fewer than 2 returns");
41 if (opt.annualize) { m.rv[s] *= opt.periods_per_year; m.bpv[s] *= opt.periods_per_year; m.rsv_neg[s] *= opt.periods_per_year; m.rsv_pos[s] *= opt.periods_per_year; m.jump[s] *= opt.periods_per_year; m.tripv[s] *= opt.periods_per_year; m.rq[s] *= opt.periods_per_year * opt.periods_per_year; } 47 if (opt.annualize) { m.rv[s] *= opt.periods_per_year; m.bpv[s] *= opt.periods_per_year; m.rsv_neg[s] *= opt.periods_per_year; m.rsv_pos[s] *= opt.periods_per_year; m.jump[s] *= opt.periods_per_year; m.tripv[s] *= opt.periods_per_year; m.rq[s] *= opt.periods_per_year * opt.periods_per_year; }
42− } 48+ } });
43 return m; 49 return m;
44 } 50 }
45 51
46 52
Menu

Theme