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 28 from wp46.realized_measures import MEASURE_COLUMNS, session_measures
29 29
30 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 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 102 for c in MEASURE_COLUMNS + ["expected_bars", "valid_rm"]:
101 103 out[c] = ir[c]
102 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 107 out["r"] = 100.0 * np.log(out["close"] / out["close"].shift(1))
105 108 out["r_on"] = 100.0 * np.log(out["open"] / out["close"].shift(1))
106 109 out.index.name = "session"
107 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 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 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