The Point-Optimal Unit Root Test and the Purchasing Power Parity Puzzle
Replication package for the Model LB test, its finite-sample calibration, and the PPP application.
Versions and the article. Release v2.0.1 (
doi:10.5281/zenodo.22088946) is the snapshot that reproduces the article. Earlier releases (v1.x, v2.0.0) correspond to earlier drafts of the article and do not reproduce the current manuscript; use the concept DOI10.5281/zenodo.21229773to resolve to the latest reproducing version.
Standard unit-root tests over-detrend when a series has exogenous level
breaks (dated regime shifts), destroying power. This package implements
Model LB — the no-trend restriction of the Carrion-i-Silvestre, Kim &
Perron (2009) point-optimal test — together with the finite-sample calibration
surface c̄(m,T) that keeps it correctly sized at the short spans macro data
actually offer. It then applies the apparatus to the purchasing power parity
puzzle on eight admissible real exchange rates, 1973–2024.
Every number in the paper is reproducible from the code plus the shipped frozen inputs, and every table and figure ships with a one-command self-check.
compute modules ──write──▶ CSV artifacts ──read──▶ figures ──▶ PDFs
(numba Monte Carlo) (traceable data) (grayscale, no simulation)
The numerical work lives in one kernel (src/mlb/kernel_numba.py). The
kernel is numba-only: there is no pure-Python fallback, because a fallback
could return a silently different calibration number on the MAIC k≤0 s²
path — and a replication package must not ship that. requirements.txt pins
numba. Computation and plotting are strictly separated: compute modules only
write CSVs; the figure code only reads them. A referee can regenerate the
CSV-backed figures from the shipped data without running any simulation.
python -m pip install -r requirements.txt # numpy, scipy, numba, matplotlib, joblib
python replicate.py # door table + every door in --quickpython replicate.py prints the door table and reproduces every printed object
from the shipped frozen inputs and golden artefacts — fast, no heavy Monte
Carlo. Add a door name to run just one; add --full to regenerate.
model_lb/model_lb_standalone.py is a self-contained CLI: point it at a CSV,
name the series, and give the known break dates.
python model_lb/model_lb_standalone.py --csv myseries.csv --col rer --date-col year \
--breaks 1985,1992 --sigma2 maic --calib inputs/cbar_surface.csv| Flag | Meaning |
|---|---|
--csv |
CSV with the series, rows in time order (required) |
--col |
series column (default: last numeric column) |
--date-col |
optional date column, to match --breaks |
--breaks |
comma-separated exogenous break dates |
--sigma2 |
long-run variance: const (difference-based) or maic |
--calib |
calibration surface (inputs/cbar_surface.csv) for the finite-sample c̄ and critical value |
--alpha, --kmax, --nsim, --json |
level, MAIC lag ceiling, on-the-fly CV reps, JSON output |
replicate.py single entry point (door table + dispatch; --quick default, --full opt-in)
src/mlb/ the numba kernel + facades + the single path resolver
kernel_numba.py @njit GLS-detrending, the five M-class statistics, calibration driver
paths.py the single resolver (inputs AND outputs); no file finds its own path
kernel_select.py the one documented kernel binding (numba-only)
design.py detrend.py dgp.py lrv.py resample.py statistics.py calibrate.py aggregate.py io.py
section_replications/ one door per theoretical layer (named by the manuscript's \label)
critical_values.py sec:calibration/cv/shortcuts: tab:cbar, tab:cv, tab:power, fig:overdetrend, fig:size
robustness.py sec:robustness: tab:ar1-sizepower, stab:recal (ar1_sizepower_recal engine)
bootstrap_empirical.py sec:empirical: tab:dates, tab:hl, tab:ppp, tab:pppsurface, fig:rerseries
supplement.py appendix S: stab:objects, stab:disp, stab:disp/L_T, sfig:density, sfig:forest
figures.py fig1..figS2 (consolidated figure rendering)
model_lb/ standalone: apply Model LB to your own series (model_lb_standalone.py)
inputs/ frozen inputs a --quick run reads (never regenerated here)
cbar_surface.csv the calibration surface (produced_outside, regenerable_here=false)
ppp_panel.csv exog_dates.csv ppp_ar_diagnostic.csv DATA_TERMS.md
limiting_density.csv Fig S1 limiting null law (frozen; renders sfig:density)
boot_out/calib/ surface_ppp_boot.csv (tab:pppsurface source)
boot_out/sensitivity/ §S5 common-nuisance / seed / lambda-exact arms (archived per the supplement)
ppp_sweep/ sweep_gates.csv -- series-by-series admissibility attrition (C1-C5)
MANIFEST.json per-file sha256, provenance_class, regenerable_here
OBJECT_MAP.csv number -> producer -> artefact -> digest (the reproduction map)
output/golden/ the promoted, frozen printed artefacts (.tex / .csv / .json)
tests/ invariance/convention tests (theta, chunk, --jobs, PQ k=0)
Every path resolves through src/mlb/paths.py; no file computes its own path,
and no door takes a free --outdir. --full writes only under output/,
never into inputs/ or output/golden/.
One entry point. --quick (default) reproduces each printed object from the
shipped frozen inputs and golden artefacts; --full regenerates the
regenerable-here objects (never the calibration surface, which is a frozen
external input).
python replicate.py # every door, --quick (fast)
python replicate.py cv # sec:calibration/cv/shortcuts
python replicate.py robustness --full # regenerate tab:ar1-sizepower + stab:recal
python replicate.py ppp # sec:empirical
python replicate.py supplement # appendix S
python replicate.py figures # fig1..figS2
python tests/run_all.py # invariance/convention gates| Exhibit (label) | Door | Frozen artefact |
|---|---|---|
tab:cbar c̄ surface |
replicate.py cv |
output/golden/printed_tables/tab_cbar_mT.tex |
tab:cv critical values (printed MZt 5%) |
replicate.py cv |
output/golden/critical_values/tabcv_reconciliation.csv |
tab:cv full M-class percentiles (1/2.5/5/10% × P_T,MZα,MSB,MZt,MPT) |
replicate.py cv --full |
output/golden/critical_values/tabcv_full_percentiles.csv |
tab:power size/power |
replicate.py cv |
output/golden/critical_values/tabpower_runC.json |
tab:ar1-sizepower AR(1) size/power |
replicate.py robustness [--full] |
output/golden/calibration_robustness/ar1_size_power.csv |
stab:recal recalibration |
replicate.py robustness [--full] |
output/golden/calibration_robustness/ar1_recalibration.csv |
tab:dates break dates |
(input) | inputs/exog_dates.csv |
tab:ppp PPP verdicts |
replicate.py ppp |
output/golden/bootstrap_empirical/ppp_empirical.csv |
tab:hl half-lives |
replicate.py ppp |
output/golden/bootstrap_empirical/hl_results_wild.csv |
tab:pppsurface applied calibration |
replicate.py ppp |
output/golden/bootstrap_empirical/tab_pppsurface.tex |
stab:objects / stab:disp modulus |
replicate.py supplement |
output/golden/supplement_appendix/modulus_feasible_*.csv |
stab:disp/L_T displacement ladder |
replicate.py supplement |
output/golden/supplement_appendix/g36_ladder.json |
fig:overdetrend (Figure 1) / fig:size (Figure 2) |
replicate.py cv --full |
regenerated (self-contained MC); data == tab:power |
fig:rerseries / sfig:density / sfig:forest |
replicate.py figures |
regenerated from the shipped CSVs |
| Source | Series | Access |
|---|---|---|
| BIS | bilateral USD nominal exchange rates (WS_XRU, annual) |
data.bis.org (bulk WS_XRU_csv_flat.zip) |
| World Bank | Consumer Price Index (FP.CPI.TOTL, 2010 = 100) |
World Bank Indicators API |
| Central banks / G-5 | exogenous regime break dates (Plaza, ERM exits, floats) | primary sources, cited in inputs/exog_dates.csv |
The panel is rebuildable from these public sources; terms and the exact series
identifiers are in inputs/DATA_TERMS.md.
The paper states the five admissibility conditions and the eight admitted currencies (§6.1); the full attrition arithmetic lives here.
A currency i belongs to the admissible universe U iff it satisfies
| Condition | Statement |
|---|---|
| (C1) | Own currency, continuously observed 1973–2024 |
| (C2) | T_i ≥ 45 |
| (C3) | max_t π_it < 0.30 (no annual CPI inflation above 30%) |
| (C4) | Var(Δq_it) conditionally stable |
| (C5) | At least one exogenously datable currency-regime event (a known-date regime in the sense of Perron 1989) |
Attrition, applied to the complete BIS annual universe of 192 area–currency series covering 147 distinct currencies:
| Stage | Removed | Remaining |
|---|---|---|
| (C1)+(C2): own-currency/continuity triage (ISO-issuer rule removing currency unions, back-calculated euro-legacy series, and non-issuer users of shared currencies; hard-peg screen) and the span condition | 122 | 70 |
| (C3): inflation ceiling | 40 | 30 |
| (C4): conditional-variance gate | 5 | 25 |
| (C5): known-date condition — 14 managed floats against unannounced baskets or de-facto pegs with no exogenously datable regime event against the dollar, plus 3 crisis-identified near-misses | 17 | 8 |
Admitted: AUD, CAD, CHF, GBP, JPY, NOK, NZD, SEK against the US dollar,
annual, 1973–2024 (T = 52).
Two operationalizations of (C4). (1) Declared gate (envelope form) —
variance-ratio, robust Levene, and a CUSUM-of-squares statistic evaluated
against the envelope of the admitted set. (2) Pre-registered absolute form
— a two-sided F test for the variance ratio, Brown–Forsythe, and the
CUSUM-of-squares in the heteroskedasticity- and dependence-robust κ₂ form
of Sansó et al. (2004), evaluated on a significance grid from 5% down to 1%.
Under the absolute form, six of the eight admitted currencies pass at
every level and seven pass at 1%; the single marginal case is the Swiss
franc.
The three near-misses.
| Currency | Why it clears the mechanical gates | Why it is excluded |
|---|---|---|
| Korean won | Passes, but narrowly: peak inflation 28.7% | 1997 event is a twin currency–banking crisis; the level shift arrives jointly with a variance shift the level-only apparatus does not model |
| Thai baht | Passes (C4) under the declared envelope | Same crisis-driven identification |
| South African rand | Passes (C4) under both operationalizations | Same crisis-driven identification |
All three are left to the variance-break extension discussed in the paper's conclusion.
The finite-sample optimum c̄(m,T) and the critical values behind it are located by a
single tangency procedure applied identically to every cell. The design is anchored on the
demeaned ERS constant: the paper proves the tangency has the form
c̄*(m,T,λ) = c̄_∞ + Δ_T with c̄_∞ = −7 and Δ_T = O((m+1)/T), so the detrending target
is flat at −7 and the finite-sample table records only the Δ_T departure, largest at the
shortest spans (up to (m+1)/T ≈ 0.10).
Grid. T ∈ {30, 45, 50, 60, 80, 100, 150, 200, 300} and m ∈ {0,…,5}, with a minimum
sample size per m (30,30,30,45,60,80 for m=0,…,5). Break fractions lie in [ε,1−ε],
trimming ε = 0.15, minimum spacing 0.15. This enumerates to 418 break configurations
for m ≥ 1; the m = 0 cell has no break locations and is a single refined search per
T (9 cells).
Tangency search. For each configuration and each candidate c̄ on the grid
{−20,−19.5,…,−3} (step 0.5): (i) R_cv replications under the null (c=0) of
y_t = Z_t'θ + u_t, u_t = (1+c/T)u_{t−1} + ε_t (with u_0 = 0, i.e. u_1 = ε_1) give
the 5% critical value of the point-optimal statistic; (ii) R_pow replications under the
alternative at c = c̄ give its rejection rate; (iii) the crossing of power 0.50 is located
by linear interpolation, with delta-method standard error se(c̄*) = se(power)/|slope|,
se(power) = √(0.25/R_pow). The criterion is power at the single point c = c̄.
Replication counts. The exploratory surface uses R_cv = 10,000, R_pow = 5,000 (code
defaults). The production surface the paper prints uses R = 200,000 for m ≥ 1 and a
refined search at R = 40,000 for m = 0, both long-run-variance arms (a simple
difference-based estimator and the autoregressive MAIC estimator of Perron & Ng). Precision
is inherited by averaging over the break-fraction configurations within each m ≥ 1 cell,
and by the refined search for m = 0; the table reports a per-cell delta-method standard
error. Treat the reported se_cbar as a lower bound on seed-to-seed uncertainty for
m ≥ 1, not its sampling standard error.
The MAIC short-T degeneracy. Under the MAIC estimator at the shortest sample sizes the power curve can fail to reach 0.50 within the grid; across the twenty-three configurations where this binds the search reports the grid value closest to 0.50 and flags the cell, rather than reporting a false precision. The estimator, not the grid, is the binding constraint there.
Long-run variance. Both estimators are applied to the OLS-detrended series, not the raw
series: the level dummies induce jumps in the raw first difference at the break dates. The
MAIC lag order is selected on the OLS regression up to k_max = 12; the spectral density
s² is then formed on the GLS-detrended residuals. (v1.3.1 formed s² on the OLS residuals
and conflated the two Ng–Perron error variances; both are corrected in v2.0.0.)
Validation: the detrending value is flat at −7. Fitting c̄*(m,T) in 1/T for
T ≥ 100, weighted by the inverse squared standard error of the mean across break
configurations, returns an intercept c̄_∞ ≈ −7 for every m alike, with a slope a(m)
increasing in m.
Replication counts of the other Monte Carlo exhibits (production values):
| Object | Replications |
|---|---|
Finite-sample critical-value table (tab:cv, full M-class 1/2.5/5/10%, c̄ = −7) |
3,300,000 (K = 20 blocks × 165,000, CRN within a block. cv --full regenerates the whole table via the same block design; the printed values reproduce within twice the simulation standard error plus the two-decimal rounding half-width (the criterion the manuscript states)) |
Size-corrected power table (tab:power) |
320,000,000 null + 80,000,000 per power entry |
AR(1) size / power robustness (tab:ar1-sizepower) |
R_eval = 10,000 (i.i.d. anchor capped at 2,000) |
Recalibration dispersion (stab:recal) |
4,000 |
PPP verdicts and half-lives (tab:ppp, tab:hl) |
sieve bootstrap B = 19,999 per currency; half-life inversion at nsim = 10⁶ |
| Sieve bootstrap surface / empirical critical values | R_cv = 10,000, R_pow = 5,000 / R_cv = 20,000 |
Empirical modulus M_T(h) (stab:objects, stab:disp) |
200,000 |
Displacement ladder L_T (stab:disp/L_T) |
1,000,000 |
Seeds are a pure function of the work item: each (P,T,m,λ) maps to a deterministic
config_seed from seed_base = 20240601; replication r draws from
default_rng((seed0 + r) mod (2⁶³ − 1)); the null stream is offset by 2·10⁶. Parallelism,
chunking, and resume are invisible in the output; the test suite verifies bit-identity
across scheduling regimes.
Table and figure captions in the paper carry the minimum needed to read them; the Monte-Carlo error accounting that a reader does not need at the table lives here.
tab:cbar(the c̄ surface). Within a cell, the break-fraction configurations are driven by one shared innovation path, so the cell mean does not average simulation error down across configurations — the reportedse_cbaris a lower bound on seed-to-seed uncertainty, not its sampling standard error (see the replication-counts note above). The last-printed-digit stability was checked directly at the rounding-boundary cell (m=2, T=150): six disjoint seed streams give a between-stream standard deviation of 0.045 with a 3/6 split across the rounding boundary, i.e. the last printed digit is stable to ±0.05.tab:cv(the M-class critical values).K = 20blocks of165,000(R = 3,300,000), block stride10⁹, with common random numbers within each block so that cross-cell differences are paired (their differencing cancels the shared error). Every cross-cell comparison the text makes is resolved at ≥ 25 standard errors.tab:power(size-corrected power). Critical values at320,000,000draws and each power entry at80,000,000, across four disjoint streams at stride10⁹. The standard error of a printed power entry decomposes as a binomial part of0.005pp within a total of0.007–0.009pp; the remainder comes from the estimated critical value it is size-corrected against.tab:ar1-sizepower(AR(1) robustness). The i.i.d. anchor is held atR = 2,000by the producer and does not improve with a larger request (the cap is intentional). The shippedoutput/golden/calibration_robustness/ar1_size_power.csvcarries the full ρ ∈ {0.3, 0.5, 0.6} grid the manuscript cites.tab:hl(median-unbiased half-lives).10⁶draws in the median-unbiased (Andrews–Chen) inversion;B = 19,999grid-t bootstrap replications per scheme (recursive and wild).
limiting_density.csv provenance. The frozen input behind Figure S1 (sfig:density)
is declared produced_outside in inputs/MANIFEST.json; it is generated by the heavy
replicate_section3_4.py --limiting-density Monte Carlo (which lives in the construction tree),
shipped frozen, and rendered — never regenerated — by the figures door.
- Deterministic given the seeds and replication counts stated in each
module header and in
MC_vs_BOOTSTRAP.md. - Invariance check. The exact θ-invariance that the paper establishes for
the GLS-detrended statistic (it does not depend on the break magnitudes at
all) doubles as the single most useful integrity check on this
implementation: because the detrended series — and hence every statistic —
is identically independent of the break magnitudes, an implementation can
be verified by generating the same innovation
u_tand recomputing the statistic atθ = 0and at a largeθ: the two must coincide to machine precision. A Monte Carlo verification acrossm ∈ {0, 1, 2}andTup to 400 confirms the invariance to ten significant digits. Run viapython tests/run_all.py. - Numba kernels are validated against
arch.DFGLSto 10 decimals; bit-identity across scheduling regimes (--jobs, chunking) is checked bytests/test_njobs_invariance.py/tests/test_chunk_invariance.py. - The analytic invariances (θ-invariance, the Perron–Qu
k=0convention, chunk- and--jobs-invariance) anchor the kernel; the package ships one kernel by design, so there is no second-implementation equivalence test. - Provenance and the per-version changelog are in
CHANGELOG.md.
hl_median_unbiased (the bootstrap_empirical door) reports two interval
schemes for the median-unbiased (Andrews–Chen) half-life estimator, both at
B = 19,999, with the half-life inversion at nsim = 10⁶:
- Grid-t bootstrap (Hansen 1999), valid uniformly in
αincluding the unit-root neighborhood (Mikusheva 2007), inverted on a grid refined to step 0.005 near the unit root. - Recursive residual bootstrap — the design-consistent baseline under the conditional-homoskedasticity gate (C4).
- Heteroskedasticity-robust wild bootstrap — Rademacher multipliers in the fixed-design construction of Cavaliere and Taylor (2008). This is the scheme reported in the half-life table.
The Pesaran (2004, 2015) CD statistic on the eight univariate Model LB
residuals rejects cross-sectional independence in both levels (+14.04) and
first differences (+8.65); the drop indicates contemporaneous dependence
rather than a common trend. This dependence is priced into the §6.3
rejection-count argument with a one-factor equicorrelated Gaussian
approximation at mean pairwise correlations 0.37–0.41, raising the probability
of zero rejections under H1-throughout from 0.006 (independence) to
0.09–0.10 (supplement door).
Carrion-i-Silvestre, Kim & Perron's publicly archived GAUSS library,
gauss-carrion-library,
dispatches the slope-only and level-and-slope specifications to the same
response-surface procedure — corroborating on the implementation side the
cancellation result behind Model LB's no-trend restriction.
If you use this software, please cite both the article and this archive
(see CITATION.cff):
Gonçalves Silva, R. (2026). Level Breaks and Finite-Sample GLS Detrending: The Point-Optimal Unit Root Test and the Purchasing Power Parity Puzzle. Replication package archived on Zenodo — concept DOI 10.5281/zenodo.21229773.
The article itself (not just this replication package) is also available as a preprint, ahead of and independent from journal review:
- SSRN: papers.ssrn.com/sol3/papers.cfm?abstract_id=7138278
- MPRA (Munich Personal RePEc Archive), paper no. 130117: mpra.ub.uni-muenchen.de/130117/
Code is released under the MIT License (LICENSE). The bundled public data
are redistributed under their original terms (BIS terms of use; World Bank CPI
under CC-BY-4.0); see the data-source notes above.
Harvard Dataverse deposit. The data-only Dataverse deposit ("Replication Data for: ...") takes a single license/terms selection for the whole dataset. Both upstream sources require attribution on redistribution — World Bank data is explicitly CC-BY-4.0, and the BIS terms of permitted use require citing the BIS as the source — so CC0 is not a correct choice for that deposit's terms; set the Dataverse dataset's terms to CC BY 4.0 to match.