SPB Git

spb/wp46_uqo

Public

WP46: registry/protocol configs, tests, runners, panel validity rules D-05/D-06

Simon-Pierre Boucher committed (Nov 9, 2026) parent ac3ba6c Browse files

23 changed files +4,518 −2

added configs/forecast_protocol.yaml +31 −0
@@ -0,0 +1,31 @@
1+# Pre-registered forecasting protocol (frozen with docs/preregistered_protocol.md; git tag prereg-frozen).
2+units: {returns: "percent log return", variance: "percent^2"}
3+horizons: [1, 5, 22] # trading days; target = variance of the h-day cumulative return
4+estimation:
5+ scheme: rolling # primary; 'expanding' and window 2000 are robustness variants
6+ window: 1000
7+ refit_every: 5 # trading days; daily filtering with fixed parameters in between
8+ max_restarts: 3
9+ seed: 46
10+ simulation_paths: 2000 # multi-step forecasts for models without an analytic recursion
11+periods:
12+ validation: {start: "2012-01-03", end: "2015-12-31"} # used for design checks and failure accounting only
13+ test: {start: "2016-01-04", end: "2026-10-30"} # untouched until the protocol is frozen
14+primary_loss: qlike
15+primary_proxy: rv5ss_on
16+secondary_losses: [mse, mae, mse_sd]
17+secondary_proxies: [rv5_on, rv1_on, bv5_on, medrv_on, r2, rv5ss_scaled]
18+tier_b_input: rv5ss_on
19+inference:
20+ dm: {hac: "Newey-West Bartlett", bandwidth: "h - 1 + floor(4 (n/100)^(2/9))"}
21+ mcs: {alpha: 0.10, bootstrap: stationary, block_length: 22, reps: 2000, statistic: "Tmax"}
22+ spa: {reps: 2000, block_length: 22}
23+ cross_asset: "block bootstrap over time with assets kept together (dependence across assets preserved)"
24+tail_risk: {levels: [0.01, 0.05], tests: [kupiec_uc, christoffersen_cc, engle_manganelli_dq, fz0_loss]}
25+regimes:
26+ trailing_vol_terciles: "terciles of the 22-day trailing realized variance of the instrument, computed at the origin (no look-ahead), thresholds from the validation period"
27+ calendar_episodes:
28+ covid: {start: "2020-02-20", end: "2020-06-30"}
29+ rate_shock_2022: {start: "2022-01-03", end: "2022-12-30"}
30+ tariff_shock_2025: {start: "2025-04-02", end: "2025-05-30"}
31+ return_sign: "sign of the previous day's return at the origin"
added configs/model_registry.yaml +55 −0
@@ -0,0 +1,55 @@
1+# WP46 model registry — the BINDING list of architectures and specifications (C1). One entry per architecture;
2+# specifications = architecture x innovation distribution x order (orders other than (1,1) only where listed).
3+# status: implemented | unsupported (no vetted implementation in the version-locked libraries; not scored) | extension.
4+# impl: arch (Sheppard, arch 8.0.0) | custom (src/wp46/models/custom.py, validated vs arch/rugarch) | ols | none.
5+# dists: innovation laws that are formally valid for the architecture AND available in the implementation.
6+# tier: A = daily returns only; B = daily returns + lagged realized measure; C = multivariate (configs/study.yaml).
7+version: 1
8+frozen_with_protocol: true
9+families:
10+ baseline: {label: "Baselines", color_key: 0}
11+ classical: {label: "Classical ARCH/GARCH", color_key: 1}
12+ asymmetric: {label: "Asymmetric", color_key: 2}
13+ power: {label: "Power / absolute", color_key: 3}
14+ fractional: {label: "Fractional persistence", color_key: 4}
15+ component: {label: "Component", color_key: 5}
16+ regime-switching: {label: "Regime switching", color_key: 6}
17+ score-driven: {label: "Score-driven (non-GARCH)", color_key: 7}
18+ realized: {label: "Realized-measure models (Tier B)", color_key: 8}
19+models:
20+ constvar: {family: baseline, tier: A, impl: arch, status: implemented, dists: [normal], label: "Constant variance (window mean)", ref: null, nested_in: garch11}
21+ histvar22: {family: baseline, tier: A, impl: ols, status: implemented, dists: [empirical], label: "Historical variance, 22 days", ref: null}
22+ histvar66: {family: baseline, tier: A, impl: ols, status: implemented, dists: [empirical], label: "Historical variance, 66 days", ref: null}
23+ histvar252: {family: baseline, tier: A, impl: ols, status: implemented, dists: [empirical], label: "Historical variance, 252 days", ref: null}
24+ ewma094: {family: baseline, tier: A, impl: arch, status: implemented, dists: [normal], label: "EWMA (RiskMetrics 1994, lambda = 0.94)", ref: jpmorgan1996riskmetrics, nested_in: igarch11}
25+ rm2006: {family: baseline, tier: A, impl: arch, status: implemented, dists: [normal], label: "RiskMetrics 2006", ref: zumbach2007riskmetrics}
26+ arch5: {family: classical, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "ARCH(5)", ref: engle1982autoregressive}
27+ garch11: {family: classical, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "GARCH(1,1)", ref: bollerslev1986generalized, benchmark: true}
28+ garch21: {family: classical, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "GARCH(2,1)", ref: bollerslev1986generalized}
29+ garch12: {family: classical, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "GARCH(1,2)", ref: bollerslev1986generalized}
30+ igarch11: {family: classical, tier: A, impl: custom, status: implemented, dists: [normal, t, ged, skewt], label: "IGARCH(1,1)", ref: engle1986modelling}
31+ garchm11: {family: classical, tier: A, impl: custom, status: implemented, dists: [normal, t, ged, skewt], label: "GARCH-M(1,1)", ref: engle1987estimating}
32+ gjr11: {family: asymmetric, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "GJR-GARCH(1,1)", ref: glosten1993relation}
33+ tarch11: {family: asymmetric, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "TGARCH(1,1) (Zakoian, threshold on sigma)", ref: zakoian1994threshold}
34+ egarch11: {family: asymmetric, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "EGARCH(1,1)", ref: nelson1991conditional}
35+ nagarch11: {family: asymmetric, tier: A, impl: custom, status: implemented, dists: [normal, t, ged, skewt], label: "NAGARCH(1,1) (Engle-Ng)", ref: engle1993measuring}
36+ qgarch11: {family: asymmetric, tier: A, impl: custom, status: implemented, dists: [normal, t, ged, skewt], label: "QGARCH(1,1) (Sentana)", ref: sentana1995quadratic}
37+ avgarch11: {family: power, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "AVGARCH(1,1) (Taylor-Schwert)", ref: taylor2008modelling}
38+ aparch11: {family: power, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "APARCH(1,1)", ref: ding1993long}
39+ figarch11: {family: fractional, tier: A, impl: arch, status: implemented, dists: [normal, t, ged, skewt], label: "FIGARCH(1,d,1)", ref: baillie1996fractionally, note: "arch truncates the ARCH(inf) representation at 1000 lags"}
40+ fiegarch11: {family: fractional, tier: A, impl: none, status: unsupported, dists: [], label: "FIEGARCH", ref: bollerslev1996modeling, note: "no vetted implementation in arch 8.0.0 / rugarch 1.5-6; not scored"}
41+ fiaparch11: {family: fractional, tier: A, impl: none, status: unsupported, dists: [], label: "FIAPARCH", ref: tse1998conditional, note: "no vetted implementation; not scored"}
42+ hygarch11: {family: fractional, tier: A, impl: none, status: unsupported, dists: [], label: "HYGARCH", ref: davidson2004moment, note: "no vetted implementation; not scored"}
43+ cgarch11: {family: component, tier: A, impl: custom, status: implemented, dists: [normal, t, ged, skewt], label: "Component GARCH (Engle-Lee)", ref: engle1999long}
44+ msgarch2: {family: regime-switching, tier: A, impl: custom, status: implemented, dists: [normal, t], label: "MS-GARCH(2) (Haas-Mittnik-Paolella)", ref: haas2004new, note: "common nu across regimes; regimes ordered by unconditional variance"}
45+ stgarch: {family: regime-switching, tier: A, impl: none, status: unsupported, dists: [], label: "Smooth-transition GARCH", ref: gonzalez1998smooth, note: "not implemented (no vetted library; deferred)"}
46+ garch_midas: {family: component, tier: A, impl: none, status: unsupported, dists: [], label: "GARCH-MIDAS", ref: engle2013stock, note: "requires a low-frequency explanatory series outside the pre-registered information set; not scored"}
47+ gas_t11: {family: score-driven, tier: A, impl: custom, status: implemented, dists: [t], label: "GAS-t(1,1) / Beta-t-GARCH (non-GARCH comparator)", ref: creal2013generalized}
48+ sv: {family: score-driven, tier: A, impl: none, status: unsupported, dists: [], label: "Stochastic volatility", ref: kim1998stochastic, note: "simulation-based estimation not feasible at the rolling re-estimation cadence of this benchmark; not scored"}
49+ rm_rw: {family: realized, tier: B, impl: ols, status: implemented, dists: [empirical], label: "Realized-measure random walk", ref: null}
50+ har_rv: {family: realized, tier: B, impl: ols, status: implemented, dists: [empirical], label: "HAR-RV (direct, levels)", ref: corsi2009simple}
51+ loghar_rv: {family: realized, tier: B, impl: ols, status: implemented, dists: [empirical], label: "log-HAR-RV (direct, smearing)", ref: corsi2009simple}
52+ realgarch11: {family: realized, tier: B, impl: custom, status: implemented, dists: [normal, t], label: "Realized GARCH(1,1), log-linear", ref: hansen2012realized}
53+ heavy11: {family: realized, tier: B, impl: custom, status: implemented, dists: [normal, t], label: "HEAVY(1,1)", ref: shephard2010realising}
54+ garchx_rv11: {family: realized, tier: B, impl: custom, status: implemented, dists: [normal, t], label: "GARCH-X(1,1) with lagged realized measure", ref: engle2002new}
55+ realegarch11: {family: realized, tier: B, impl: none, status: unsupported, dists: [], label: "Realized EGARCH", ref: hansen2016realized, note: "not implemented in this version (deferred)"}
added configs/study.yaml +11 −0
@@ -0,0 +1,11 @@
1+# Study-level settings (frozen with the protocol).
2+title: "Four Decades of Conditional Volatility Modeling: A Comprehensive Empirical Benchmark Across Assets, Frequencies, and Market Regimes"
3+wp_number: 46
4+data_source: {name: "FinData OS (FirstRate Data)", base_url: "https://findata.spboucher.ai/api/v1", license: "frd-private"}
5+sample: {start: "2000-01-03", end: "2026-10-30"}
6+universe_file: configs/instruments.yaml
7+tier_c_baskets:
8+ equity_etf: [SPY, QQQ, IWM, DIA]
9+ cross_asset_etf: [SPY, TLT, GLD, USO, EEM]
10+tier_c_models: [rolling_cov252, ewma_cov094, ccc_garch11, dcc11, adcc11, scalar_bekk11]
11+tier_c_horizons: [1]
added results/metrics/rugarch_crosscheck.json +769 −0
@@ -0,0 +1,769 @@
1+{
2+ "rugarch": {
3+ "rugarch_version": "1.5.6",
4+ "garch11_t": {
5+ "converged": true,
6+ "loglik": -3379.50279602402,
7+ "coef": {
8+ "mu": 0.0272520855544271,
9+ "omega": 0.0174152246241149,
10+ "alpha1": 0.0766441622812045,
11+ "beta1": 0.897925769946588,
12+ "shape": 6.92334228660823
13+ },
14+ "sigma2_last5": [
15+ 0.331218279727791,
16+ 0.332580388365619,
17+ 0.386557106650745,
18+ 0.41489230427875,
19+ 0.395462742973217
20+ ],
21+ "fc": [
22+ 0.373396880254564,
23+ 0.381316596907874,
24+ 0.389034914629954,
25+ 0.396556955009274,
26+ 0.403887709391963,
27+ 0.411032042193878,
28+ 0.417994694128454,
29+ 0.424780285352459,
30+ 0.431393318531763,
31+ 0.437838181829138,
32+ 0.444119151816078,
33+ 0.450240396310574,
34+ 0.456205977142725,
35+ 0.462019852850014,
36+ 0.467685881304048,
37+ 0.473207822270496,
38+ 0.478589339903933,
39+ 0.483834005179235,
40+ 0.488945298261143,
41+ 0.493926610813575,
42+ 0.498781248250204,
43+ 0.503512431927809
44+ ]
45+ },
46+ "gjr11_t": {
47+ "converged": true,
48+ "loglik": -3372.99049098085,
49+ "coef": {
50+ "mu": 0.0223490291478538,
51+ "omega": 0.0174956405075909,
52+ "alpha1": 0.0426372227127993,
53+ "beta1": 0.900398468982572,
54+ "gamma1": 0.0620237392157647,
55+ "shape": 7.00993566707479
56+ },
57+ "sigma2_last5": [
58+ 0.332335215434199,
59+ 0.340484527422025,
60+ 0.419369166473932,
61+ 0.463058113854574,
62+ 0.441677271920104
63+ ],
64+ "fc": [
65+ 0.416282527426611,
66+ 0.422974621160647,
67+ 0.429493038742298,
68+ 0.435842287491262,
69+ 0.442026757751298,
70+ 0.448050725926038,
71+ 0.453918357436013,
72+ 0.45963370959893,
73+ 0.465200734435209,
74+ 0.470623281400702,
75+ 0.475905100048492,
76+ 0.481049842621618,
77+ 0.486061066578505,
78+ 0.490942237052855,
79+ 0.495696729249701,
80+ 0.500327830779275,
81+ 0.504838743930304,
82+ 0.509232587884315,
83+ 0.513512400872467,
84+ 0.51768114227641,
85+ 0.521741694674624,
86+ 0.525696865835649
87+ ]
88+ },
89+ "egarch11_t": {
90+ "converged": true,
91+ "loglik": -3376.68334976327,
92+ "coef": {
93+ "mu": 0.0188171893640434,
94+ "omega": -0.0170849884697369,
95+ "alpha1": -0.0444147835573704,
96+ "beta1": 0.968708686881274,
97+ "gamma1": 0.156456196163122,
98+ "shape": 6.99039734118426
99+ },
100+ "sigma2_last5": [
101+ 0.328885250801329,
102+ 0.350822965161747,
103+ 0.436863315480211,
104+ 0.499447742972208,
105+ 0.479693397387017
106+ ],
107+ "fc": [
108+ 0.440981758288812,
109+ 0.444761569483677,
110+ 0.448453995963031,
111+ 0.452060109871317,
112+ 0.455581032094569,
113+ 0.45901792729419,
114+ 0.462371999182192,
115+ 0.4656444860341,
116+ 0.468836656435033,
117+ 0.471949805253863,
118+ 0.474985249839914,
119+ 0.477944326436176,
120+ 0.480828386802756,
121+ 0.483638795043929,
122+ 0.486376924632017,
123+ 0.489044155621135,
124+ 0.491641872043739,
125+ 0.494171459482879,
126+ 0.49663430281301,
127+ 0.499031784102273,
128+ 0.501365280669172,
129+ 0.503636163286671
130+ ]
131+ },
132+ "aparch11_t": {
133+ "converged": true,
134+ "loglik": -3372.84892807275,
135+ "coef": {
136+ "mu": 0.0219498032140538,
137+ "omega": 0.0182869465555667,
138+ "alpha1": 0.0734928958434684,
139+ "beta1": 0.901720469927853,
140+ "gamma1": 0.230203192227028,
141+ "delta": 1.84060080476714,
142+ "shape": 7.0279389234871
143+ },
144+ "sigma2_last5": [
145+ 0.332682598703389,
146+ 0.342687583247142,
147+ 0.423457812590537,
148+ 0.47031035284744,
149+ 0.448241954551476
150+ ],
151+ "fc": [
152+ 0.420729778911146,
153+ 0.426946162214478,
154+ 0.433001778704925,
155+ 0.438900519070086,
156+ 0.444646200595584,
157+ 0.450242566618973,
158+ 0.455693286232804,
159+ 0.461001954204718,
160+ 0.46617209108646,
161+ 0.471207143487254,
162+ 0.476110484489945,
163+ 0.48088541419098,
164+ 0.485535160347515,
165+ 0.490062879116947,
166+ 0.494471655875874,
167+ 0.498764506106986,
168+ 0.502944376343734,
169+ 0.507014145163756,
170+ 0.510976624223076,
171+ 0.514834559323999,
172+ 0.518590631510407,
173+ 0.522247458184874
174+ ]
175+ },
176+ "figarch11_t": {
177+ "converged": true,
178+ "loglik": -3381.97980336414,
179+ "coef": {
180+ "mu": 0.028208711165193,
181+ "omega": 0.0134114399389171,
182+ "alpha1": 0.26282947219305,
183+ "beta1": 0.780139313197165,
184+ "delta": 0.631140189523463,
185+ "shape": 6.41803885552558
186+ },
187+ "sigma2_last5": [
188+ 0.299696988917735,
189+ 0.316330648317056,
190+ 0.390432077409812,
191+ 0.384885199689091,
192+ 0.343022844513784
193+ ],
194+ "fc": [
195+ 0.339059269752477,
196+ 0.367590095570308,
197+ 0.368843743107776,
198+ 0.370160945435298,
199+ 0.373457632868578,
200+ 0.377938383945126,
201+ 0.382963124329593,
202+ 0.388198567929454,
203+ 0.393484081477849,
204+ 0.398744620082659,
205+ 0.403945640040183,
206+ 0.409071431842686,
207+ 0.414115967772256,
208+ 0.419078001693844,
209+ 0.423958280305449,
210+ 0.42875861007803,
211+ 0.433481497208184,
212+ 0.438129181396169,
213+ 0.442704763673238,
214+ 0.447212036971553,
215+ 0.451653257516078,
216+ 0.456031281967143
217+ ]
218+ },
219+ "igarch11_t": {
220+ "converged": true,
221+ "loglik": -3385.51714991758,
222+ "coef": {
223+ "mu": 0.0273555996213812,
224+ "omega": 0.00822664842815519,
225+ "alpha1": 0.0911285633973949,
226+ "beta1": 0.908871436602605,
227+ "shape": 5.98132454596413
228+ },
229+ "sigma2_last5": [
230+ 0.301577365761645,
231+ 0.303442040727677,
232+ 0.367868964109828,
233+ 0.402485492370637,
234+ 0.380584653228751
235+ ],
236+ "fc": [
237+ 0.355184003263419,
238+ 0.363410651691574,
239+ 0.371637300119729,
240+ 0.379863948547884,
241+ 0.388090596976039,
242+ 0.396317245404194,
243+ 0.40454389383235,
244+ 0.412770542260505,
245+ 0.42099719068866,
246+ 0.429223839116815,
247+ 0.43745048754497,
248+ 0.445677135973126,
249+ 0.453903784401281,
250+ 0.462130432829436,
251+ 0.470357081257591,
252+ 0.478583729685746,
253+ 0.486810378113901,
254+ 0.495037026542057,
255+ 0.503263674970212,
256+ 0.511490323398367,
257+ 0.519716971826522,
258+ 0.527943620254678
259+ ]
260+ },
261+ "cgarch11_t": {
262+ "converged": true,
263+ "loglik": -3379.76186721388,
264+ "coef": {
265+ "mu": 0.0273402099302754,
266+ "omega": 0.0178411646698263,
267+ "alpha1": 0.0472240690102781,
268+ "beta1": 0.926336229155417,
269+ "eta11": 0.973560849178106,
270+ "eta21": 0.0299126275164861,
271+ "shape": 6.89529147023611
272+ },
273+ "sigma2_last5": [
274+ 0.331806400268661,
275+ 0.333156933397685,
276+ 0.387466733404178,
277+ 0.415888052702624,
278+ 0.3961973946619
279+ ],
280+ "fc": [
281+ 0.373894792219397,
282+ 0.410734909756006,
283+ 0.417716681277378,
284+ 0.424513858333285,
285+ 0.431131321506913,
286+ 0.437573822341324,
287+ 0.443845986751214,
288+ 0.449952318344464,
289+ 0.455897201655865,
290+ 0.461684905295354,
291+ 0.467319585013005,
292+ 0.472805286682989,
293+ 0.478145949208632,
294+ 0.483345407350674,
295+ 0.488407394480744,
296+ 0.493335545262035,
297+ 0.498133398259106,
298+ 0.50280439847868,
299+ 0.507351899843266,
300+ 0.511779167599379,
301+ 0.516089380662085,
302+ 0.520285633897565
303+ ]
304+ },
305+ "nagarch11_t": {
306+ "converged": true,
307+ "loglik": -3373.35050140171,
308+ "coef": {
309+ "mu": 0.0213271608385587,
310+ "omega": 0.0169277508131017,
311+ "alpha1": 0.0706508464116506,
312+ "beta1": 0.895992701235594,
313+ "eta21": 0.339825862746243,
314+ "shape": 7.06871565857494
315+ },
316+ "sigma2_last5": [
317+ 0.320085472046233,
318+ 0.335215163198902,
319+ 0.410709126156738,
320+ 0.458799430333668,
321+ 0.445128729120879
322+ ],
323+ "fc": [
324+ 0.423373945827428,
325+ 0.429633698461851,
326+ 0.435735720488707,
327+ 0.441683986337342,
328+ 0.447482370291107,
329+ 0.453134649010792,
330+ 0.458644503994484,
331+ 0.464015523975432,
332+ 0.469251207259498,
333+ 0.474354964003706,
334+ 0.479330118437384,
335+ 0.484179911027331,
336+ 0.488907500588426,
337+ 0.493515966341068,
338+ 0.498008309916754,
339+ 0.502387457313138,
340+ 0.50665626079982,
341+ 0.510817500776111,
342+ 0.514873887581995,
343+ 0.518828063263449,
344+ 0.522682603293291,
345+ 0.526440018248661
346+ ]
347+ },
348+ "tgarch11_t": {
349+ "converged": true,
350+ "loglik": -3378.26565819207,
351+ "coef": {
352+ "mu": 0.0185075851662991,
353+ "omega": 0.0237242658452512,
354+ "alpha1": 0.0806721422971265,
355+ "beta1": 0.908415590277261,
356+ "eta11": 0.278262852619424,
357+ "shape": 7.02369203281082
358+ },
359+ "sigma2_last5": [
360+ 0.339303507806645,
361+ 0.361929596403121,
362+ 0.446541788728846,
363+ 0.509029924970293,
364+ 0.488014547239253
365+ ],
366+ "fc": [
367+ 0.446902528383541,
368+ 0.451531432310373,
369+ 0.456042749975163,
370+ 0.460438686643197,
371+ 0.464721461954639,
372+ 0.468893304636195,
373+ 0.472956447662954,
374+ 0.476913123839437,
375+ 0.480765561770887,
376+ 0.484515982197637,
377+ 0.488166594667179,
378+ 0.49171959452015,
379+ 0.495177160168002,
380+ 0.498541450641537,
381+ 0.501814603390865,
382+ 0.504998732318567,
383+ 0.508095926029051,
384+ 0.511108246278208,
385+ 0.514037726608485,
386+ 0.516886371155493,
387+ 0.519656153613176,
388+ 0.522349016345428
389+ ]
390+ },
391+ "realgarch11_n": {
392+ "converged": false
393+ },
394+ "garch11_skewt": {
395+ "converged": true,
396+ "loglik": -3379.31479592696,
397+ "coef": {
398+ "mu": 0.0247602319116539,
399+ "omega": 0.017295740459104,
400+ "alpha1": 0.0762213395583014,
401+ "beta1": 0.89844516796877,
402+ "skew": 0.984514907132941,
403+ "shape": 6.94374052154004
404+ },
405+ "sigma2_last5": [
406+ 0.330905238634026,
407+ 0.332071373500861,
408+ 0.385400191269274,
409+ 0.413348758343739,
410+ 0.394040260337795
411+ ],
412+ "fc": [
413+ 0.372159534757372,
414+ 0.380027174443972,
415+ 0.387695499339791,
416+ 0.395169558784582,
417+ 0.402454274200687,
418+ 0.40955444233363,
419+ 0.41647473841062,
420+ 0.423219719219034,
421+ 0.429793826106908,
422+ 0.436201387907421,
423+ 0.442446623789292,
424+ 0.448533646034957,
425+ 0.454466462748379,
426+ 0.460248980494249,
427+ 0.465885006870329,
428+ 0.471378253014633,
429+ 0.476732336049089,
430+ 0.481950781461291,
431+ 0.487037025425924,
432+ 0.491994417067363,
433+ 0.496826220664968,
434+ 0.501535617802502
435+ ]
436+ }
437+ },
438+ "ours": {
439+ "garch11_t": {
440+ "loglik": -3379.461796995996,
441+ "params": {
442+ "mu": 0.0272552165475274,
443+ "omega": 0.017418392193745573,
444+ "alpha[1]": 0.0766713206199158,
445+ "beta[1]": 0.8979047644168446,
446+ "nu": 6.915683563286161
447+ },
448+ "converged": true
449+ },
450+ "gjr11_t": {
451+ "loglik": -3372.953632113916,
452+ "params": {
453+ "mu": 0.02234711621886731,
454+ "omega": 0.017500497241068884,
455+ "alpha[1]": 0.04266800772043423,
456+ "gamma[1]": 0.062034959552290915,
457+ "beta[1]": 0.9003610818426231,
458+ "nu": 7.002485146679242
459+ },
460+ "converged": true
461+ },
462+ "egarch11_t": {
463+ "loglik": -3376.6704308851477,
464+ "params": {
465+ "mu": 0.018828915964200128,
466+ "omega": -0.011034952859929866,
467+ "alpha[1]": 0.15651669565747273,
468+ "gamma[1]": -0.044438618764715755,
469+ "beta[1]": 0.9686789065580167,
470+ "nu": 6.98363424230133
471+ },
472+ "converged": true
473+ },
474+ "aparch11_t": {
475+ "loglik": -3372.7593223523118,
476+ "params": {
477+ "mu": 0.021873566385943646,
478+ "omega": 0.01845096728792146,
479+ "alpha[1]": 0.0741123489743683,
480+ "gamma[1]": 0.23185125233775555,
481+ "beta[1]": 0.9019093525442575,
482+ "delta": 1.810851742298129,
483+ "nu": 7.017009293700917
484+ },
485+ "converged": true
486+ },
487+ "figarch11_t": {
488+ "loglik": -3383.4506039107964,
489+ "params": {
490+ "mu": 0.027341455003779366,
491+ "omega": 0.018557454873431037,
492+ "phi": 0.2272419820409831,
493+ "d": 0.545516035918098,
494+ "beta": 0.6927227001090212,
495+ "nu": 6.4546568465574365
496+ },
497+ "converged": true
498+ },
499+ "garch11_skewt": {
500+ "loglik": -3379.275642878361,
501+ "params": {
502+ "mu": 0.0247698562671931,
503+ "omega": 0.017301334377567416,
504+ "alpha[1]": 0.07625585117671085,
505+ "beta[1]": 0.8984117763911253,
506+ "eta": 6.936304710964621,
507+ "lambda": -0.01552668380206145
508+ },
509+ "converged": true
510+ },
511+ "igarch11_t": {
512+ "loglik": -3385.466983993589,
513+ "params": {
514+ "mu": 0.027354323480404286,
515+ "omega": 0.008236861969721824,
516+ "alpha": 0.09116543468488557,
517+ "nu": 5.973513461861548
518+ },
519+ "converged": true
520+ },
521+ "cgarch11_t": {
522+ "loglik": -3379.461678802403,
523+ "params": {
524+ "mu": 0.027257000911121945,
525+ "omega": 0.01736733788836256,
526+ "rho": 0.9746416050735823,
527+ "phi": 0.03393172704779641,
528+ "alpha": 0.04272735073115051,
529+ "beta": 0.9317694810875947,
530+ "nu": 6.9169232979627395
531+ },
532+ "converged": true
533+ },
534+ "nagarch11_t": {
535+ "loglik": -3373.3123413949697,
536+ "params": {
537+ "mu": 0.02133256452316757,
538+ "omega": 0.016934592629425642,
539+ "alpha": 0.07068774289404209,
540+ "theta": 0.3397290537185268,
541+ "beta": 0.8959513510919638,
542+ "nu": 7.06163608387285
543+ },
544+ "converged": true
545+ },
546+ "realgarch11_n": {
547+ "loglik": -4989.212643221531,
548+ "params": {
549+ "mu": 0.01677936657297113,
550+ "omega": -0.0310098669169496,
551+ "beta": 0.7043221645737289,
552+ "gamma": 0.2567538036768552,
553+ "xi": 0.04191198028547544,
554+ "phi": 1.0024048779109547,
555+ "tau1": 0.0010267719765093414,
556+ "tau2": 0.013224579404035812,
557+ "log_sigma_u": -0.9137863869883917
558+ },
559+ "converged": true
560+ }
561+ },
562+ "comparison": {
563+ "garch11_t": {
564+ "rugarch_loglik": -3379.50279602402,
565+ "ours_loglik": -3379.461796995996,
566+ "rugarch_coef": {
567+ "mu": 0.0272520855544271,
568+ "omega": 0.0174152246241149,
569+ "alpha1": 0.0766441622812045,
570+ "beta1": 0.897925769946588,
571+ "shape": 6.92334228660823
572+ },
573+ "ours_params": {
574+ "mu": 0.0272552165475274,
575+ "omega": 0.017418392193745573,
576+ "alpha[1]": 0.0766713206199158,
577+ "beta[1]": 0.8979047644168446,
578+ "nu": 6.915683563286161
579+ },
580+ "diff": 0.04099902802408906
581+ },
582+ "gjr11_t": {
583+ "rugarch_loglik": -3372.99049098085,
584+ "ours_loglik": -3372.953632113916,
585+ "rugarch_coef": {
586+ "mu": 0.0223490291478538,
587+ "omega": 0.0174956405075909,
588+ "alpha1": 0.0426372227127993,
589+ "beta1": 0.900398468982572,
590+ "gamma1": 0.0620237392157647,
591+ "shape": 7.00993566707479
592+ },
593+ "ours_params": {
594+ "mu": 0.02234711621886731,
595+ "omega": 0.017500497241068884,
596+ "alpha[1]": 0.04266800772043423,
597+ "gamma[1]": 0.062034959552290915,
598+ "beta[1]": 0.9003610818426231,
599+ "nu": 7.002485146679242
600+ },
601+ "diff": 0.036858866934380785
602+ },
603+ "egarch11_t": {
604+ "rugarch_loglik": -3376.68334976327,
605+ "ours_loglik": -3376.6704308851477,
606+ "rugarch_coef": {
607+ "mu": 0.0188171893640434,
608+ "omega": -0.0170849884697369,
609+ "alpha1": -0.0444147835573704,
610+ "beta1": 0.968708686881274,
611+ "gamma1": 0.156456196163122,
612+ "shape": 6.99039734118426
613+ },
614+ "ours_params": {
615+ "mu": 0.018828915964200128,
616+ "omega": -0.011034952859929866,
617+ "alpha[1]": 0.15651669565747273,
618+ "gamma[1]": -0.044438618764715755,
619+ "beta[1]": 0.9686789065580167,
620+ "nu": 6.98363424230133
621+ },
622+ "diff": 0.01291887812249115
623+ },
624+ "aparch11_t": {
625+ "rugarch_loglik": -3372.84892807275,
626+ "ours_loglik": -3372.7593223523118,
627+ "rugarch_coef": {
628+ "mu": 0.0219498032140538,
629+ "omega": 0.0182869465555667,
630+ "alpha1": 0.0734928958434684,
631+ "beta1": 0.901720469927853,
632+ "gamma1": 0.230203192227028,
633+ "delta": 1.84060080476714,
634+ "shape": 7.0279389234871
635+ },
636+ "ours_params": {
637+ "mu": 0.021873566385943646,
638+ "omega": 0.01845096728792146,
639+ "alpha[1]": 0.0741123489743683,
640+ "gamma[1]": 0.23185125233775555,
641+ "beta[1]": 0.9019093525442575,
642+ "delta": 1.810851742298129,
643+ "nu": 7.017009293700917
644+ },
645+ "diff": 0.08960572043815773
646+ },
647+ "figarch11_t": {
648+ "rugarch_loglik": -3381.97980336414,
649+ "ours_loglik": -3383.4506039107964,
650+ "rugarch_coef": {
651+ "mu": 0.028208711165193,
652+ "omega": 0.0134114399389171,
653+ "alpha1": 0.26282947219305,
654+ "beta1": 0.780139313197165,
655+ "delta": 0.631140189523463,
656+ "shape": 6.41803885552558
657+ },
658+ "ours_params": {
659+ "mu": 0.027341455003779366,
660+ "omega": 0.018557454873431037,
661+ "phi": 0.2272419820409831,
662+ "d": 0.545516035918098,
663+ "beta": 0.6927227001090212,
664+ "nu": 6.4546568465574365
665+ },
666+ "diff": -1.4708005466563918
667+ },
668+ "igarch11_t": {
669+ "rugarch_loglik": -3385.51714991758,
670+ "ours_loglik": -3385.466983993589,
671+ "rugarch_coef": {
672+ "mu": 0.0273555996213812,
673+ "omega": 0.00822664842815519,
674+ "alpha1": 0.0911285633973949,
675+ "beta1": 0.908871436602605,
676+ "shape": 5.98132454596413
677+ },
678+ "ours_params": {
679+ "mu": 0.027354323480404286,
680+ "omega": 0.008236861969721824,
681+ "alpha": 0.09116543468488557,
682+ "nu": 5.973513461861548
683+ },
684+ "diff": 0.05016592399078945
685+ },
686+ "cgarch11_t": {
687+ "rugarch_loglik": -3379.76186721388,
688+ "ours_loglik": -3379.461678802403,
689+ "rugarch_coef": {
690+ "mu": 0.0273402099302754,
691+ "omega": 0.0178411646698263,
692+ "alpha1": 0.0472240690102781,
693+ "beta1": 0.926336229155417,
694+ "eta11": 0.973560849178106,
695+ "eta21": 0.0299126275164861,
696+ "shape": 6.89529147023611
697+ },
698+ "ours_params": {
699+ "mu": 0.027257000911121945,
700+ "omega": 0.01736733788836256,
701+ "rho": 0.9746416050735823,
702+ "phi": 0.03393172704779641,
703+ "alpha": 0.04272735073115051,
704+ "beta": 0.9317694810875947,
705+ "nu": 6.9169232979627395
706+ },
707+ "diff": 0.3001884114769382
708+ },
709+ "nagarch11_t": {
710+ "rugarch_loglik": -3373.35050140171,
711+ "ours_loglik": -3373.3123413949697,
712+ "rugarch_coef": {
713+ "mu": 0.0213271608385587,
714+ "omega": 0.0169277508131017,
715+ "alpha1": 0.0706508464116506,
716+ "beta1": 0.895992701235594,
717+ "eta21": 0.339825862746243,
718+ "shape": 7.06871565857494
719+ },
720+ "ours_params": {
721+ "mu": 0.02133256452316757,
722+ "omega": 0.016934592629425642,
723+ "alpha": 0.07068774289404209,
724+ "theta": 0.3397290537185268,
725+ "beta": 0.8959513510919638,
726+ "nu": 7.06163608387285
727+ },
728+ "diff": 0.038160006740326935
729+ },
730+ "garch11_skewt": {
731+ "rugarch_loglik": -3379.31479592696,
732+ "ours_loglik": -3379.275642878361,
733+ "rugarch_coef": {
734+ "mu": 0.0247602319116539,
735+ "omega": 0.017295740459104,
736+ "alpha1": 0.0762213395583014,
737+ "beta1": 0.89844516796877,
738+ "skew": 0.984514907132941,
739+ "shape": 6.94374052154004
740+ },
741+ "ours_params": {
742+ "mu": 0.0247698562671931,
743+ "omega": 0.017301334377567416,
744+ "alpha[1]": 0.07625585117671085,
745+ "beta[1]": 0.8984117763911253,
746+ "eta": 6.936304710964621,
747+ "lambda": -0.01552668380206145
748+ },
749+ "diff": 0.0391530485990188
750+ },
751+ "realgarch11_n": {
752+ "rugarch_loglik": null,
753+ "ours_loglik": -4989.212643221531,
754+ "rugarch_coef": null,
755+ "ours_params": {
756+ "mu": 0.01677936657297113,
757+ "omega": -0.0310098669169496,
758+ "beta": 0.7043221645737289,
759+ "gamma": 0.2567538036768552,
760+ "xi": 0.04191198028547544,
761+ "phi": 1.0024048779109547,
762+ "tau1": 0.0010267719765093414,
763+ "tau2": 0.013224579404035812,
764+ "log_sigma_u": -0.9137863869883917
765+ },
766+ "diff": null
767+ }
768+ }
769+}
\ No newline at end of file
modified scripts/audit_findata.py +0 −0
modified scripts/build_panels.py +0 −0
modified scripts/build_references.py +0 −0
added scripts/dispatch_nodes.py +70 −0
@@ -0,0 +1,70 @@
1+#!/usr/bin/env python3
2+"""Dispatch a job list over the MacLustr nodes (core-proportional shards) and collect results.
3+usage: dispatch_nodes.py launch --jobs results/manifests/jobs_main.json --name main [--nodes M3U96a:26,M3U96b:26,...]
4+ dispatch_nodes.py status --name main
5+ dispatch_nodes.py collect --name main # rsync <node>:~/wp46-cache/forecasts_<name>/ -> results/forecasts/<name>/
6+Panels (daily returns + realized measures) must exist on every node under ~/wp46-cache/panels (synced here)."""
7+from __future__ import annotations
8+
9+import argparse
10+import json
11+import subprocess
12+from pathlib import Path
13+
14+ROOT = Path(__file__).resolve().parents[1]
15+DEFAULT_NODES = "M3U96a:24,M3U96b:26,M2U64:22,M5M36:16,M4M64a:14,M4M64c:14"
16+
17+
18+def sh(cmd: list[str], timeout=600) -> str:
19+ return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout
20+
21+
22+def main() -> None:
23+ ap = argparse.ArgumentParser()
24+ ap.add_argument("cmd", choices=["launch", "status", "collect", "sync-panels"])
25+ ap.add_argument("--jobs", default="")
26+ ap.add_argument("--name", required=True)
27+ ap.add_argument("--nodes", default=DEFAULT_NODES)
28+ a = ap.parse_args()
29+ nodes = [(n.split(":")[0], int(n.split(":")[1])) for n in a.nodes.split(",")]
30+ if a.cmd == "sync-panels":
31+ for n, _ in nodes:
32+ print(n, sh(["rsync", "-aq", "--delete", str(Path.home() / "wp46-cache" / "panels") + "/", f"{n}:~/wp46-cache/panels/"]))
33+ return
34+ if a.cmd == "launch":
35+ jobs = json.loads(Path(a.jobs).read_text())
36+ tot = sum(c for _, c in nodes)
37+ # proportional contiguous shards over the (heavy-first) job order -> interleave: node k takes jobs with
38+ # index % N in its share; simpler and balanced: assign job i to node by weighted round robin
39+ assign = {n: [] for n, _ in nodes}
40+ weights = [c / tot for _, c in nodes]
41+ acc = [0.0] * len(nodes)
42+ for i, j in enumerate(jobs):
43+ k = max(range(len(nodes)), key=lambda q: weights[q] * (i + 1) - acc[q])
44+ acc[k] += 1
45+ assign[nodes[k][0]].append(j)
46+ for n, cores in nodes:
47+ jf = ROOT / "results" / "manifests" / f"jobs_{a.name}_{n}.json"
48+ jf.write_text(json.dumps(assign[n]))
49+ subprocess.run(["scp", "-q", str(jf), f"{n}:~/wp46_uqo/results/manifests/"], check=True)
50+ print(n, cores, len(assign[n]), sh(["ssh", n, f"bash ~/wp46_uqo/scripts/node_run.sh results/manifests/jobs_{a.name}_{n}.json 0/1 {cores} {a.name}"]).strip())
51+ return
52+ if a.cmd == "status":
53+ for n, _ in nodes:
54+ out = sh(["ssh", n, f"cd ~/wp46_uqo && (wc -l < ~/wp46-cache/forecasts_{a.name}/_runs.jsonl 2>/dev/null || echo 0); "
55+ f"python3 -c \"import json;print(len(json.load(open('results/manifests/jobs_{a.name}_{n}.json'))))\" 2>/dev/null; "
56+ f"grep -c error ~/wp46-cache/forecasts_{a.name}/_runs.jsonl 2>/dev/null; uptime | sed 's/.*load/load/'; "
57+ f"pgrep -fc run_benchmark.py"])
58+ print(n, " | ".join(x.strip() for x in out.splitlines()))
59+ return
60+ if a.cmd == "collect":
61+ dest = ROOT / "results" / "forecasts" / a.name
62+ dest.mkdir(parents=True, exist_ok=True)
63+ for n, _ in nodes:
64+ print(n, sh(["rsync", "-aq", f"{n}:~/wp46-cache/forecasts_{a.name}/", str(dest) + "/"], timeout=3600))
65+ runs = [json.loads(l) for l in (dest / "_runs.jsonl").read_text().splitlines()] if (dest / "_runs.jsonl").exists() else []
66+ print("collected runs:", len(runs), "errors:", sum("error" in r for r in runs))
67+
68+
69+if __name__ == "__main__":
70+ main()
modified scripts/ingest_bars.py +0 −0
added scripts/make_jobs.py +61 −0
@@ -0,0 +1,61 @@
1+#!/usr/bin/env python3
2+"""Expand the frozen configs into a job list (instrument x model x distribution x estimation variant).
3+usage: make_jobs.py --name main [--tier A,B] [--symbols ...] [--oos-start 2012-01-03] [--oos-end 2026-10-30]
4+ [--window 1000] [--refit 5] [--expanding] [--models m1,m2]"""
5+from __future__ import annotations
6+
7+import argparse
8+import json
9+import sys
10+from pathlib import Path
11+
12+import yaml
13+
14+ROOT = Path(__file__).resolve().parents[1]
15+sys.path.insert(0, str(ROOT / "src"))
16+
17+
18+def main() -> None:
19+ ap = argparse.ArgumentParser()
20+ ap.add_argument("--name", required=True)
21+ ap.add_argument("--tier", default="A,B")
22+ ap.add_argument("--symbols", default="")
23+ ap.add_argument("--models", default="")
24+ ap.add_argument("--oos-start", default=None)
25+ ap.add_argument("--oos-end", default=None)
26+ ap.add_argument("--window", type=int, default=None)
27+ ap.add_argument("--refit", type=int, default=None)
28+ ap.add_argument("--expanding", action="store_true")
29+ a = ap.parse_args()
30+ reg = yaml.safe_load(open(ROOT / "configs" / "model_registry.yaml"))["models"]
31+ inst = yaml.safe_load(open(ROOT / "configs" / "instruments.yaml"))
32+ prot = yaml.safe_load(open(ROOT / "configs" / "forecast_protocol.yaml"))
33+ uni = inst.get("universe") or inst["candidates"]
34+ syms = [s for cls in uni for s in uni[cls]]
35+ if a.symbols:
36+ syms = [s for s in a.symbols.split(",") if s]
37+ tiers = a.tier.split(",")
38+ oos_start = a.oos_start or prot["periods"]["validation"]["start"]
39+ oos_end = a.oos_end or prot["periods"]["test"]["end"]
40+ window = a.window or prot["estimation"]["window"]
41+ refit = a.refit or prot["estimation"]["refit_every"]
42+ want = set(a.models.split(",")) if a.models else None
43+ jobs = []
44+ for s in syms:
45+ for mid, m in reg.items():
46+ if m["status"] != "implemented" or m["tier"] not in tiers or (want and mid not in want):
47+ continue
48+ for dist in m["dists"]:
49+ jobs.append(dict(symbol=s, model_id=mid, dist=dist, tier=m["tier"], window=window, refit_every=refit,
50+ expanding=a.expanding, rm_proxy=prot["tier_b_input"], oos_start=oos_start, oos_end=oos_end,
51+ seed=prot["estimation"]["seed"], max_restarts=prot["estimation"]["max_restarts"]))
52+ # heavy jobs first (better load balance): msgarch, figarch, realgarch, aparch/egarch (simulation forecasts)
53+ rank = {"msgarch2": 0, "figarch11": 1, "realgarch11": 2, "aparch11": 3, "egarch11": 4, "tarch11": 5, "avgarch11": 6}
54+ jobs.sort(key=lambda j: (rank.get(j["model_id"], 9), j["symbol"]))
55+ out = ROOT / "results" / "manifests" / f"jobs_{a.name}.json"
56+ out.write_text(json.dumps(jobs, indent=0))
57+ print(out, len(jobs), "jobs;", len(syms), "symbols;", sorted({j['model_id'] for j in jobs}))
58+
59+
60+if __name__ == "__main__":
61+ main()
added scripts/node_run.sh +11 −0
@@ -0,0 +1,11 @@
1+#!/bin/bash
2+# Run one shard of a job list on a cluster node (called over ssh from scripts/dispatch_nodes.py).
3+# usage: node_run.sh <jobs_file_relative> <shard i/N> <workers> <run_name>
4+set -u
5+cd ~/wp46_uqo || exit 1
6+git pull -q
7+mkdir -p results/logs
8+export WP46_CACHE=~/wp46-cache
9+nohup .venv/bin/python scripts/run_benchmark.py --jobs "$1" --shard "$2" --workers "$3" --out ~/wp46-cache/forecasts_"$4" \
10+ > results/logs/bench_"$4"_shard_"${2/\//_}".log 2>&1 &
11+echo "started shard $2 on $(hostname) pid $!"
added scripts/run_benchmark.py +96 −0
@@ -0,0 +1,96 @@
1+#!/usr/bin/env python3
2+"""Run rolling-origin forecasting jobs (instrument x model x distribution) and write one Parquet per job.
3+
4+usage: run_benchmark.py --jobs results/manifests/jobs_<name>.json [--shard i/N] [--workers 8] [--out results/forecasts]
5+The job list is produced by scripts/make_jobs.py from the frozen configs. Each job writes
6+ <out>/<symbol>/<spec_id>.parquet (forecasts, one row per origin)
7+ <out>/<symbol>/<spec_id>.fits.parquet (refit attempts)
8+and a line in <out>/_runs.jsonl (git SHA, host, package versions, wall time). Existing outputs are skipped (resumable).
9+Nothing printed contains return values."""
10+from __future__ import annotations
11+
12+import argparse
13+import json
14+import os
15+import platform
16+import resource
17+import socket
18+import subprocess
19+import sys
20+import time
21+from concurrent.futures import ProcessPoolExecutor, as_completed
22+from pathlib import Path
23+
24+ROOT = Path(__file__).resolve().parents[1]
25+sys.path.insert(0, str(ROOT / "src"))
26+
27+
28+def git_sha() -> str:
29+ try:
30+ return subprocess.check_output(["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"], text=True).strip()
31+ except Exception: # noqa: BLE001
32+ return "unknown"
33+
34+
35+def run_one(job: dict, out: str) -> dict:
36+ import numpy as np
37+ import pandas as pd
38+ from wp46.estimation import JobSpec, run_job
39+ from wp46.preprocessing import load_panel
40+ js = JobSpec(**{k: v for k, v in job.items() if k in JobSpec.__dataclass_fields__})
41+ d = Path(out) / js.symbol
42+ d.mkdir(parents=True, exist_ok=True)
43+ fp = d / f"{js.spec_id}.parquet"
44+ if fp.exists():
45+ return {"spec_id": js.spec_id, "symbol": js.symbol, "skipped": True}
46+ t0 = time.perf_counter()
47+ panel = load_panel(js.symbol)
48+ F, G = run_job(panel, js)
49+ F.to_parquet(fp, index=False)
50+ G.to_parquet(d / f"{js.spec_id}.fits.parquet", index=False)
51+ ru = resource.getrusage(resource.RUSAGE_SELF)
52+ return {"spec_id": js.spec_id, "symbol": js.symbol, "n_origins": int(len(F)), "n_fits": int(len(G)),
53+ "status_counts": F["status"].value_counts().to_dict() if len(F) else {},
54+ "fit_status_counts": G["status"].value_counts().to_dict() if len(G) else {},
55+ "wall_s": time.perf_counter() - t0, "cpu_s": ru.ru_utime + ru.ru_stime, "maxrss_mb": ru.ru_maxrss / (1024 ** 2 if platform.system() == "Darwin" else 1024),
56+ "fit_time_total_s": float(G["time_s"].sum()) if len(G) else 0.0, "fc_time_total_s": float(F["time_fc_s"].sum()) if "time_fc_s" in F else 0.0}
57+
58+
59+def main() -> None:
60+ ap = argparse.ArgumentParser()
61+ ap.add_argument("--jobs", required=True)
62+ ap.add_argument("--shard", default="0/1")
63+ ap.add_argument("--workers", type=int, default=max(1, os.cpu_count() - 2))
64+ ap.add_argument("--out", default=str(ROOT / "results" / "forecasts"))
65+ a = ap.parse_args()
66+ jobs = json.loads(Path(a.jobs).read_text())
67+ i, n = map(int, a.shard.split("/"))
68+ jobs = [j for k, j in enumerate(jobs) if k % n == i]
69+ import arch, numpy, scipy, pandas
70+ meta = {"git_sha": git_sha(), "host": socket.gethostname(), "python": platform.python_version(), "arch": arch.__version__,
71+ "numpy": numpy.__version__, "scipy": scipy.__version__, "pandas": pandas.__version__, "machine": platform.machine(),
72+ "cpu_count": os.cpu_count(), "shard": a.shard, "n_jobs": len(jobs), "started": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
73+ Path(a.out).mkdir(parents=True, exist_ok=True)
74+ log = Path(a.out) / "_runs.jsonl"
75+ print(json.dumps(meta), flush=True)
76+ done = 0
77+ t0 = time.time()
78+ with ProcessPoolExecutor(a.workers) as ex:
79+ futs = {ex.submit(run_one, j, a.out): j for j in jobs}
80+ for f in as_completed(futs):
81+ j = futs[f]
82+ try:
83+ rec = f.result()
84+ except Exception as e: # noqa: BLE001
85+ rec = {"spec_id": j.get("model_id"), "symbol": j.get("symbol"), "error": f"{type(e).__name__}: {str(e)[:200]}"}
86+ rec.update({"git_sha": meta["git_sha"], "host": meta["host"], "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z")})
87+ with open(log, "a") as fh:
88+ fh.write(json.dumps(rec) + "\n")
89+ done += 1
90+ if done % 10 == 0 or "error" in rec:
91+ print(f"[{done}/{len(jobs)} {time.time()-t0:7.0f}s] {rec.get('symbol')} {rec.get('spec_id')} {rec.get('error','') or rec.get('wall_s','')}", flush=True)
92+ print("done", done, "jobs in", round(time.time() - t0), "s")
93+
94+
95+if __name__ == "__main__":
96+ main()
modified src/wp46/preprocessing/panel.py +12 −2
@@ -28,7 +28,9 @@ from wp46.findata_client.ingest import DAILY_FOR, DATASET_FOR, cache_root
28 from wp46.realized_measures import MEASURE_COLUMNS, session_measures 28 from wp46.realized_measures import MEASURE_COLUMNS, session_measures
29 29
30 PANEL_COLUMNS = ["session", "close", "open", "r", "r_on", "r_oc", "valid_rm", "expected_bars"] + MEASURE_COLUMNS 30 PANEL_COLUMNS = ["session", "close", "open", "r", "r_on", "r_oc", "valid_rm", "expected_bars"] + MEASURE_COLUMNS
31−EXPECTED = {"futures": 1380, "fx": 1440, "crypto": 1440} 31+EXPECTED = {"futures": 1380, "fx": 1440, "crypto": 1440} # nominal; replaced by the instrument's modal bar count (D-05)
32+RM_VALID_FROM = {"crypto": "2020-01-01"} # D-06: crypto 1-minute bars before 2020 are contaminated by cross-venue bounce
33+ # (audit: 1-min RV 10-100x the squared daily return in 2013-2019) -> no realized measure
32 34
33 35
34 def _read_cache(dataset_id: str, symbol: str, cols: list[str]) -> pd.DataFrame: 36 def _read_cache(dataset_id: str, symbol: str, cols: list[str]) -> pd.DataFrame:
@@ -100,12 +102,20 @@ def build_daily_panel(asset_class: str, symbol: str) -> pd.DataFrame:
100 for c in MEASURE_COLUMNS + ["expected_bars", "valid_rm"]: 102 for c in MEASURE_COLUMNS + ["expected_bars", "valid_rm"]:
101 out[c] = ir[c] 103 out[c] = ir[c]
102 # drop stub sessions (Sunday evenings, holiday fragments): they are not trading days for the daily model 104 # drop stub sessions (Sunday evenings, holiday fragments): they are not trading days for the daily model
103− out = out[out["n_bars"] >= 0.25 * out["expected_bars"]] 105+ modal = int(out["n_bars"].round(-1).mode().iloc[0])
106+ out = out[out["n_bars"] >= 0.25 * modal]
104 out["r"] = 100.0 * np.log(out["close"] / out["close"].shift(1)) 107 out["r"] = 100.0 * np.log(out["close"] / out["close"].shift(1))
105 out["r_on"] = 100.0 * np.log(out["open"] / out["close"].shift(1)) 108 out["r_on"] = 100.0 * np.log(out["open"] / out["close"].shift(1))
106 out.index.name = "session" 109 out.index.name = "session"
107 out = out.iloc[1:] # first return undefined 110 out = out.iloc[1:] # first return undefined
111+ if asset_class not in ("etf", "stock"):
112+ # D-05: expected bars = modal session length of THIS instrument (grains trade ~12.5 h, metals ~22 h, FX 24 h)
113+ modal = int(out["n_bars"].round(-1).mode().iloc[0])
114+ out["expected_bars"] = modal
115+ out["valid_rm"] = out["n_bars"] >= MIN_FRACTION * modal
108 out["valid_rm"] = out["valid_rm"] & np.isfinite(out["rv_5m_ss"]) & (out["rv_5m_ss"] > 0) 116 out["valid_rm"] = out["valid_rm"] & np.isfinite(out["rv_5m_ss"]) & (out["rv_5m_ss"] > 0)
117+ if asset_class in RM_VALID_FROM:
118+ out.loc[out.index < pd.Timestamp(RM_VALID_FROM[asset_class]), "valid_rm"] = False
109 return out[PANEL_COLUMNS[1:]].reset_index() 119 return out[PANEL_COLUMNS[1:]].reset_index()
110 120
111 121
added tests/__init__.py +0 −0
added tests/conftest.py +4 −0
@@ -0,0 +1,4 @@
1+import sys
2+from pathlib import Path
3+
4+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
added tests/integration/test_rolling_driver.py +38 −0
@@ -0,0 +1,38 @@
1+import numpy as np
2+import pandas as pd
3+
4+from wp46.estimation import JobSpec, run_job
5+
6+
7+def synthetic_panel(T=1400, seed=0):
8+ rng = np.random.default_rng(seed)
9+ s = np.empty(T); r = np.empty(T); s[0] = 1.0
10+ for t in range(T):
11+ if t > 0:
12+ s[t] = 0.05 + 0.08 * r[t - 1] ** 2 + 0.9 * s[t - 1]
13+ r[t] = np.sqrt(s[t]) * rng.standard_normal()
14+ rv = s * rng.lognormal(0, 0.3, T)
15+ dates = pd.bdate_range("2010-01-04", periods=T)
16+ return pd.DataFrame({"session": dates, "r": r, "r_on": np.zeros(T), "rv_5m_ss": rv, "rv_5m": rv, "rv_1m": rv, "bv_5m": rv,
17+ "medrv_5m": rv, "valid_rm": True, "close": 100.0, "open": 100.0, "r_oc": r, "expected_bars": 391})
18+
19+
20+def test_driver_refit_cadence_and_outputs():
21+ p = synthetic_panel()
22+ F, G = run_job(p, JobSpec("SYN", "garch11", "normal", "A", window=1000, refit_every=5, oos_start="2013-01-01", oos_end="2016-12-31"))
23+ assert (F.status == "ok").all()
24+ assert len(G) == int(np.ceil(len(F) / 5))
25+ assert np.all(F.h5 > F.h1) and np.all(F.h22 > F.h5)
26+ # origins strictly after the first `window` observations
27+ assert F.origin.min() > p.session.iloc[999]
28+
29+
30+def test_driver_tier_b_and_failure_flags():
31+ p = synthetic_panel()
32+ F, G = run_job(p, JobSpec("SYN", "har_rv", "empirical", "B", window=1000, oos_start="2013-01-01", oos_end="2014-12-31"))
33+ assert (F.status == "ok").all() and np.isfinite(F.h1).all()
34+ # a missing return inside the window is reported as missing_input, never silently filled
35+ p2 = p.copy(); p2.loc[1100, "r"] = np.nan
36+ F2, _ = run_job(p2, JobSpec("SYN", "garch11", "normal", "A", window=1000, oos_start="2013-01-01", oos_end="2014-12-31"))
37+ affected = (F2.origin >= p.session.iloc[1100]) & (F2.origin <= p.session.iloc[2099])
38+ assert (F2.status == "missing_input").sum() == int(affected.sum()) and affected.sum() > 0
added tests/replication/fixture_returns.csv +3001 −0
Large diff (3,001 lines) is not loaded by default to keep the page fast.Load diff
added tests/replication/rugarch_crosscheck.R +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added tests/replication/test_rugarch.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added tests/simulation/test_recovery.py +89 −0
Large diff (89 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/test_custom_models.py +89 −0
Large diff (89 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/test_distributions.py +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/test_scoring_and_panel.py +53 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme