Structural VAR by sign restrictions — curing the price puzzle¶
Vector autoregressions, Part 2: credible shock identification¶
Part 1 (the macro and yield-curve BVAR applications) identified structural shocks the easy way — a recursive (Cholesky) ordering — and paid the price: the macro impulse responses showed the notorious price puzzle (inflation rising after a monetary tightening). Part 2 fixes it with sign restrictions (Uhlig 2005): instead of imposing a questionable ordering, we impose only the signs economics is confident about, and keep every structural rotation consistent with them.
We build this on top of the reduced-form posterior from bvar.py in a new, separate module — svar_sign.py — that imports the engine and adds the rotation search (the original bvar.py is left untouched). Two applications:
- Macro — a monetary-policy shock, identified agnostically, that removes the price puzzle.
- Finance — the Treasury curve's level and slope shocks.
The identification problem and the sign-restriction algorithm¶
Why identification is needed. The reduced-form VAR delivers correlated innovations $\varepsilon_t\sim N(0,\Sigma)$. Economic structural shocks $u_t$ (uncorrelated, unit-variance) relate to them by $\varepsilon_t = P\,u_t$ with $PP'=\Sigma$. But $P$ is not unique: for any orthogonal $Q$ ($QQ'=I$), $P=\operatorname{chol}(\Sigma)\,Q$ also satisfies $PP'=\Sigma$. The likelihood cannot distinguish these rotations — the structural model is set-identified. A Cholesky ordering just picks the one lower-triangular $P$ (i.e. $Q=I$); its price is a strong, often dubious, timing assumption.
Sign restrictions. Rather than pick one $Q$, impose only the IRF signs theory pins down — a contractionary monetary shock has the policy rate up and inflation down — and accept every rotation consistent with them. The result is not a single IRF but an identified set, reported as a posterior band.
The algorithm (rotation sampling; Rubio-Ramírez, Waggoner & Zha 2010) — for each reduced-form posterior draw $(\Pi,\Sigma)$ from bvar.py:
- Form the reduced-form MA impulse responses $\Theta_h$ (from the companion matrix) and $P_0=\operatorname{chol}(\Sigma)$.
- Draw a Haar-uniform impulse direction: $q=z/\lVert z\rVert$, $z\sim N(0,I_m)$ (a random unit vector; a column of a Haar-random orthogonal matrix).
- The candidate shock's IRF is $\Theta_h\,(P_0 q)$. Check the sign restrictions over the imposed horizons — trying $q$ and $-q$, since both are valid rotations. Keep it if satisfied, else redraw.
Collecting the accepted IRFs across draws gives the posterior over admissible responses. Agnostic identification (Uhlig's key idea): restrict only what theory dictates — rate $\uparrow$, inflation $\downarrow$ — and leave the variable of real interest, output, unrestricted, then read off what the data say about it rather than assuming the answer. All of this is svar_sign.py (sign_irf, bands).
What "the response of a variable to a shock" means. A structural shock is a one-time, unexpected impulse to a single economic driver at date $t$ — a surprise monetary tightening, say — with the other structural shocks held at zero. The impulse response is its consequence traced forward through time: the response of variable $Y$ at horizon $h$ is the change the shock induces in $Y_{t+h}$, propagated through the VAR's own dynamics. It is read in the variable's own units (percentage points here) for a shock of a fixed size (a one-standard-deviation structural shock; for the yield curve, scaled to the imposed impact pattern). Each panel below plots that whole path $h=0,1,2,\dots$ — the impact, the propagation, and, in a stationary system, the eventual decay back to zero.
Prior hyperparameters. The reduced-form Minnesota prior uses $\lambda_1=0.2$ (overall tightness) and $\lambda_3=1$ (lag decay). For the cross-variable shrinkage $\lambda_2$ we use the conventional scalar $\lambda_2=0.5$ (Litterman) on the heterogeneous macro system — rather than a correlation-based rule, which would over-shrink genuine lagged links between weakly-correlated variables — while the highly-correlated yield curve keeps the correlation-based $\lambda_2$, where that choice is well-justified (see the sensitivity in Part 1).
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.api import VAR
import bvar, svar_sign as sr
md = sm.datasets.macrodata.load_pandas().data
Ym = np.column_stack([400*np.diff(np.log(md["realgdp"].values)), 400*np.diff(np.log(md["cpi"].values)), md["tbilrate"].values[1:]])
mlab = ["GDP growth", "Inflation", "T-bill"]; p = 2; H = 20
fit = bvar.filter_stationary(bvar.gibbs_minnesota(Ym, p=p, lam2=0.5, ndraw=4000, burn=1000, seed=1)) # macro: scalar lam2=0.5
# Uhlig-agnostic contractionary monetary shock: rate UP, inflation DOWN for 4 quarters; OUTPUT LEFT UNRESTRICTED
res = sr.sign_irf(fit, [(2, +1, 3), (1, -1, 3)], H=H, max_tries=400, seed=0)
lo, med, hi = sr.bands(res)
chol = VAR(pd.DataFrame(Ym, columns=mlab)).fit(p).irf(H).orth_irfs[:, :, 2] # recursive IRF to the T-bill shock
print("MACRO: an admissible rotation found for %.0f%% of posterior draws" % (100 * res["accept_frac"]))
print(" sign-identified inflation IRF (h=0,4,8): %s (forced <=0: no price puzzle)" % np.round(med[[0,4,8],1], 2))
print(" Cholesky inflation IRF (h=0,4,8): %s (the PRICE PUZZLE: rises)" % np.round(chol[[0,4,8],1], 2))
print(" output (UNRESTRICTED) IRF (h=0,4,8): %s (Uhlig: weak / uncertain)" % np.round(med[[0,4,8],0], 2))
fig, ax = plt.subplots(1, 3, figsize=(14, 3.6))
for i, l in enumerate(mlab):
ax[i].fill_between(range(H+1), lo[:, i], hi[:, i], color="firebrick", alpha=.2, label="sign-restricted 68% band")
ax[i].plot(med[:, i], color="firebrick", lw=1.6, label="sign-restricted median")
ax[i].plot(chol[:, i], color="navy", lw=1.3, ls="--", label="Cholesky (recursive)")
ax[i].axhline(0, color="0.6", lw=.6); ax[i].set_title("Response of %s" % l); ax[i].set_xlabel("quarters")
ax[1].legend(fontsize=7, loc="upper right")
fig.suptitle("Sign-restricted vs recursive monetary shock: the sign restriction FORCES inflation down (no price puzzle);\n"
"output is left unrestricted (Uhlig-agnostic) and its effect is weak/uncertain", y=1.06)
plt.tight_layout(); plt.show()
MACRO: an admissible rotation found for 100% of posterior draws sign-identified inflation IRF (h=0,4,8): [-1.23 -0.09 0.03] (forced <=0: no price puzzle) Cholesky inflation IRF (h=0,4,8): [0. 0.27 0.2 ] (the PRICE PUZZLE: rises) output (UNRESTRICTED) IRF (h=0,4,8): [ 0.93 0.06 -0.01] (Uhlig: weak / uncertain)
The price puzzle, cured¶
- Inflation (middle panel) is the whole story. Under the recursive ordering (navy dashed) inflation rises after a tightening — the price puzzle, an artefact of assuming the Fed doesn't see prices within the quarter. The sign restriction (red) forces the response non-positive over the first year, and the identified set has it fall sharply and revert — the economically sensible disinflation.
- Output is left to speak (Uhlig-agnostic). We restricted only the rate and inflation; output is unrestricted, and its identified-set band is wide and straddles zero — the celebrated Uhlig result that the real effects of monetary shocks are far less certain than a recursive VAR pretends.
- The rate response (right) is positive by construction and dies out — the tightening itself.
- What the acceptance line means. "An admissible rotation found for 100% of draws" is the share of posterior draws for which at least one sign-consistent rotation turned up within the search budget — not the per-rotation acceptance rate, which is far lower (~15%, as the R cross-check reports directly). Sign restrictions are selective: most random rotations are rejected, and the band above is the median and spread over only the accepted ones.
Finance application — level and slope shocks in the yield curve¶
The same rotation search decomposes the Treasury yield curve — the four tenors (3-month, 5-, 10-, 30-year) modelled in Part 1. Yield-curve movements are famously low-dimensional: since Litterman & Scheinkman (1991), just three factors — level, slope, and curvature — explain ~99% of the variation in yields. A level move shifts every yield in the same direction (a parallel repricing of the whole curve); a slope move tilts it (the short and long ends move oppositely — a steepening or flattening); curvature bends the belly against the two wings.
Those factors are normally extracted by principal components — a purely statistical rotation: orthogonal, but unlabelled and static (a fixed loading vector). Sign restrictions let us recover the same shapes as structural shocks, which buys two things PCA cannot: (i) an economic label baked in by the sign pattern we impose, and (ii) the VAR's full dynamic propagation — how the curve evolves in the months after the shock, not merely its impact loading. We identify:
- a level shock — every yield restricted up on impact for six months (a repricing of the whole curve, e.g. a shift in expected inflation or the expected path of policy);
- a slope shock — the 3-month up but the 30-year down (a flattening, e.g. a tightening of the monetary stance or a compression of the term premium), leaving the intermediate tenors free to fall where they will.
Each is a separate rotation search on the reduced-form posterior (a "level-up-everywhere" restriction and a "short-up/long-down" restriction), reported below as the median response of every tenor.
dfy = pd.read_csv("bvar_yields_data.csv", parse_dates=["date"], index_col="date")
ycols = ["y3m", "y5y", "y10y", "y30y"]; ylab = ["3-month", "5-year", "10-year", "30-year"]
Yy = dfy[ycols].values
fy = bvar.filter_stationary(bvar.gibbs_minnesota(Yy, p=p, own_mean=0.9, ndraw=4000, burn=1000, seed=1))
lev = sr.sign_irf(fy, [(0, +1, 5), (1, +1, 5), (2, +1, 5), (3, +1, 5)], H=24, max_tries=600, seed=0) # LEVEL: all up
slp = sr.sign_irf(fy, [(0, +1, 5), (3, -1, 5)], H=24, max_tries=600, seed=0) # SLOPE: short up, long down
print("YIELDS: an admissible rotation found for %.0f%% of draws (level), %.0f%% (slope)" % (100*lev["accept_frac"], 100*slp["accept_frac"]))
print(" LEVEL shock impact across curve:", np.round(sr.bands(lev)[1][0], 2), "(near-parallel shift up)")
print(" SLOPE shock impact across curve:", np.round(sr.bands(slp)[1][0], 2), "(short up, long down = flattening)")
fig, ax = plt.subplots(1, 2, figsize=(12, 3.6))
for res_, title, axi in [(lev, "LEVEL shock (all yields up)", 0), (slp, "SLOPE shock (short up, long down)", 1)]:
md2 = sr.bands(res_)[1]
for i, l in enumerate(ylab): ax[axi].plot(md2[:, i], lw=1.4, label=l)
ax[axi].axhline(0, color="0.6", lw=.6); ax[axi].set_title(title); ax[axi].set_xlabel("months")
ax[0].set_ylabel("yield response (%)"); ax[0].legend(fontsize=7)
fig.suptitle("Yield-curve responses to sign-identified LEVEL and SLOPE shocks", y=1.03)
plt.tight_layout(); plt.show()
YIELDS: an admissible rotation found for 100% of draws (level), 100% (slope) LEVEL shock impact across curve: [0.09 0.16 0.14 0.11] (near-parallel shift up) SLOPE shock impact across curve: [ 0.09 -0.03 -0.07 -0.08] (short up, long down = flattening)
Reading the curve shocks¶
- Level (left) — a near-parallel repricing. All four yields jump up and stay up: the whole curve reprices together, the signature of a shift in expected inflation or in the expected path of policy. It is not perfectly parallel — the belly (5-year, ~0.16) moves a touch more than the 3-month (~0.09) and the 30-year (~0.11) — because the sign restriction fixes only the direction of each response, not exact parallelism, so a small dose of curvature leaks in. The absence of any snap-back over two years reflects the near-unit-root behaviour of yields established in Part 1 (
own_mean = 0.9): a level repricing sticks rather than mean-reverting. - Slope (right) — a flattening driven by the front end. The 3-month rises (+0.09) while the long tenors fall (down to −0.08 at the 30-year), compressing the 10y−3m spread. The magnitude ordering is the economics: the short end is the most policy-sensitive and the long end the most anchored — exactly the FEVD/Granger picture from Part 1 (a short-rate shock explained 45% of the 3-month's variance but only 11% of the 30-year's; the short/medium tenors Granger-led while the long end was near-exogenous). A flattening of this shape is the classic late-cycle / tightening signal, and — as Part 1's term-structure figure showed — the kind of move that shades into the inversions preceding the 2001, 2008 and 2020 recessions.
- What sign-identification buys over PCA. The two shocks are orthogonal and carry economic names, and each arrives with a full impulse-response path and honest posterior bands — a structural, dynamic reading of the curve rather than a static statistical rotation. The trade-off is that they are only set-identified (a level shock is any rotation with all-positive loadings), so the bands are the honest price of not over-claiming.
Results¶
- Sign restrictions replace an arbitrary ordering with a defensible belief. On the macro system they dissolve the price puzzle — inflation falls after a tightening, as it must under the restriction — while honestly reporting that the output effect is uncertain (the agnostic band spans zero). This is the modern-macro answer to "which shock is which": impose only the signs, accept the whole identified set.
- The same machine decomposes the yield curve into economically-labelled level and slope shocks (detailed above) — the two dominant factors of term-structure movements, recovered as sign-identified structural shocks with dynamics and uncertainty, not static PCA loadings.
- Set- vs point-identification. Every panel is a band, not a line, because the data leave a set of admissible rotations. That honesty — reporting what the restrictions do and do not pin down — is the point of the method, and it sits naturally on the Bayesian posterior
bvar.pyalready produces.
References¶
- Uhlig, H. (2005). What are the effects of monetary policy on output? Results from an agnostic identification procedure. J. Monetary Economics 52, 381–419.
- Rubio-Ramírez, J., Waggoner, D. & Zha, T. (2010). Structural vector autoregressions: theory of identification and algorithms for inference. Review of Economic Studies 77, 665–696.
- Sims, C. A. (1992). Interpreting the macroeconomic time series facts: the effects of monetary policy. European Economic Review 36, 975–1000 (the price puzzle).
- Litterman, R. & Scheinkman, J. (1991). Common factors affecting bond returns. J. Fixed Income 1, 54–61 (the level / slope / curvature factors).
Next: bvar_signrestrict_R.ipynb — the R cross-check.