🇬🇧 English · 🇪🇸 Español
HANK sequence-space calculations and aggregate-risk demonstrations
PureMacro contains several distinct HANK-related interfaces. Their numerical and economic scope differs. A converged residual or quadratic volatility scaling check alone does not establish a solved heterogeneous-household economy.
Choose the appropriate model
| Interface | Calculation | Validation scope |
|---|---|---|
solve_hank_sequence_space |
One-asset household EGM, stationary distribution and household sequence-space Jacobians | Matched-grid comparison with the SSJ authors' implementation |
User-supplied BlockDAG |
Composition and solution of the supplied blocks | Depends on the actual household equations and equilibrium closure |
build_canonical_hank_dag and default solve_hank_nonlinear_transition |
Reduced nonlinear model with prescribed MPC and constraint-mass formulas | Demonstration; no household optimization or distribution solve |
solve_hank_aggregate_risk_order2 |
Second-order algebra for prescribed transition matrices and moments | Reduced demonstration; not a general Reiter/Winberry or two-asset HANK solver |
solve_hank_stochastic_policy |
Solved first-order stochastic policy with compressed distribution states | Schur equations, full-state comparisons and held-out nonlinear audits |
solve_hank_stochastic_tree |
Nonlinear one- or two-asset finite histories with a solved local terminal policy | Household budgets, both applicable asset markets, goods and terminal consistency |
The aggregate-risk function rejects a supplied model or steady_state
because those inputs are not implemented. Calling it with model=None emits
a warning and records validation_scope="reduced_demonstration" and
household_distribution_solved=False in its result metadata.
Household benchmark
The one-asset model solves a backward EGM step and a forward distribution
lottery. At the steady state, household assets determine government debt.
Interest payments are financed by a proportional labor tax, so net earnings
are (w - r*B/N)*e. These fiscal assumptions must be matched in a comparison.
import numpy as np
from puremacro.models.hank_sequence_space import solve_hank_sequence_space
households = solve_hank_sequence_space(T=24, n_a=100, a_max=100.0)
assert households.ss_converged
assert abs(households.distribution.sum()-1.0) < 1e-10
print("Consumption:", households.steady_state_consumption)
print("Upper-grid mass:", households.distribution[-1].sum())
jacobian = households.fake_news(T=24, shock_input="r").jacobian
assert jacobian.shape == (24, 24)
tools/reference_validation/validate_hank_reference.py compares policies,
distributions, aggregate consumption, and wage/return Jacobians against
the authors' SSJ code at a pinned revision.
The external package is a validation-only dependency.
The benchmark uses 50 and 100 asset points with a maximum asset level of 100, two income states, and a 24-period horizon. Policies and consumption agree within \(5\times10^{-11}\); return Jacobians differ by up to \(4\times10^{-4}\) with a finite-difference step of \(10^{-4}\). The benchmark tolerance is \(5\times10^{-4}\) for return Jacobians and tighter for policies and wages.
At the smaller default maximum asset level of 30, positive mass reaches the upper boundary. The two implementations handle that boundary differently: policy differences reach about 0.015 on the 50-point grid. Widen and refine the grid, inspect tail mass, and check economic quantities before use. Agreement between implementations on a common grid is not grid convergence.
The model's aggregate closure clears goods using its stated NK equations. This benchmark validates the household block, not the complete calibration or equilibrium of a published HANK paper. Realized-return household Jacobians also differ in timing from the model's ex-ante rate Jacobian.
Nonlinear DAG calculations
For a supplied DAG, the transition solver seeks a path satisfying its residual equations, using damped quasi-Newton updates. The economic content comes from those equations. In the default reduced DAG, constraint fractions and MPCs are prescribed response functions; they are not statistics of a solved distribution.
from puremacro.dsge import solve_hank_nonlinear_transition
reduced = solve_hank_nonlinear_transition(
shocks={"eps_m": 1e-4}, horizon=25, tol=1e-8,
)
assert reduced.converged
print("Reduced-model residual:", reduced.residual_norm)
A small-shock comparison tests local consistency of this reduced system. It does not validate household Euler equations, asset-market clearing, or large-shock global accuracy.
Aggregate-risk algebra demonstration
from puremacro.dsge import solve_hank_aggregate_risk_order2
risk_demo = solve_hank_aggregate_risk_order2(sigma_agg=0.01)
assert risk_demo.metadata["validation_scope"] == "reduced_demonstration"
assert risk_demo.metadata["household_distribution_solved"] is False
print("Prescribed-system wealth shift:", risk_demo.precautionary_shift_wealth)
The matrices, moment values and curvature coefficients in this example are prescribed. Doubling volatility quadruples a quadratic correction by construction. That check does not establish endogenous precautionary saving or a quantitative policy effect.
Sequence-Space Optimal Policy & Risky Steady State (puremacro.dsge)
For linear-quadratic sequence-space Ramsey planning, optimal simple rules (OSR), and second-order Risky Steady State (RSS) / precautionary ZLB fixed points:
import numpy as np
from puremacro.dsge.ssj_policy import ssj_ramsey, ssj_osr, ssj_welfare_decomposition
from puremacro.dsge.risky_steady_state import solve_risky_steady_state, solve_precautionary_obc
# 1. Sequence-space Ramsey optimal policy (McKay & Wolf 2023 / Bhandari et al. 2021)
T = 16
J_pi = -0.35 * np.triu(0.75 ** np.subtract.outer(np.arange(T), np.arange(T)).T)
J_y = -0.60 * np.eye(T)
ramsey = ssj_ramsey(
jacobians={"pi": J_pi, "y": J_y},
target_weights={"pi": 1.0, "y": 0.25},
baseline_paths={"pi": 0.8 ** np.arange(T), "y": -0.5 * (0.7 ** np.arange(T))},
instrument_weight=0.05,
beta=0.99,
)
print(ramsey.summary())
# 2. Risky Steady State (Coeurdacier, Rey & Winant 2011)
rss = solve_risky_steady_state(
f_residual=lambda y_c, y_n, u: np.array([y_c[0] ** (-2.0) - (y_n[0] + u[0]) ** (-2.0) - (y_c[0] - 1.0)]),
y_det=np.array([1.0]),
shock_cov=np.array([[0.02 ** 2]]),
state_names=("consumption",),
)
print(rss.summary())
flowchart TD
A[Research question] --> B[Household EGM and distribution]
B --> C[Independent policies and Jacobian benchmark]
C --> D[Grid and horizon convergence]
D --> E[Documented equilibrium closure]
E --> F[Policy study with sensitivity analysis]
G[Reduced DAG and risk demonstrations] --> H[Algebra and API examples]
Next research milestone
The published one-asset equilibrium and local stochastic policies and two-asset closure now have separate implementations and validation records. They retain their stated grid, compression and approximation-order limits. Global aggregate-risk accuracy and higher-order precautionary or welfare calculations require additional methods and independent references. The reduced second-order demonstration above retains its original scope.