🇬🇧 English · 🇪🇸 Español
Recursive HANK policies and two-asset equilibrium
solve_hank_stochastic_policy solves an infinite-horizon, first-order local
rational-expectations policy around a heterogeneous-household steady state.
The state contains distribution coefficients, the inherited nominal interest
rate and a persistent monetary disturbance. Household marginal values and
aggregate prices respond jointly to that state. Future aggregate innovations
continue indefinitely.
The construction follows the projection/perturbation distinction in Reiter (2009): solve nonlinear household heterogeneity first, then linearize aggregate dynamics. The author's working paper explains why a few distribution moments need not suffice. PureMacro uses constrained distribution directions and an ordered generalized Schur decomposition. This implementation is a local solution, with no second-order precautionary shift, risk premium calculation or global stochastic welfare claim.
Solve, compress and audit
import numpy as np
from puremacro.models import (
calibrate_one_asset_hank, solve_hank_stochastic_policy,
solve_hank_stochastic_tree, monetary_scenario_tree,
)
# Demonstration grid; household-grid accuracy needs separate refinement.
ss = calibrate_one_asset_hank(n_a=80, n_e=3, a_max=150.)
full = solve_hank_stochastic_policy(ss, distribution_rank=None)
rng = np.random.default_rng(1201)
training = full.simulate(rng.normal(0, 1e-7, 160)).distributions
policy = solve_hank_stochastic_policy(
ss, distribution_rank=16, snapshots=training,
rho=.7, shock_std=1e-4,
)
# These innovations were not used to fit the distribution basis.
held_out = np.random.default_rng(9917).normal(0, 1e-4, 48)
audit = policy.audit(held_out, tolerance=1e-5)
print(audit.converged, audit.diagnostics)
tree = monetary_scenario_tree(
horizon=8, persistence=.7,
innovations=(-.001, .001), probabilities=(.3, .7),
)
equilibrium = solve_hank_stochastic_tree(policy, tree)
print(equilibrium.converged, equilibrium.diagnostics)
With no snapshots, the solver constructs directions from household price
responses and repeated distribution transport. Supplied snapshots instead
train an SVD basis around the steady distribution. Both retain the directions
needed to preserve contemporaneous market observables before reducing the
remaining variation. A rank below that minimum is rejected. Neither method fits the
equilibrium policy coefficients to a regression: those coefficients solve the
linearized household, distribution, market and price-setting equations.
distribution_rank=None retains the full constrained distribution, providing
a comparison for compression error.
The one-asset basis preserves each income marginal and aggregate inherited
assets. The two-asset basis preserves each income marginal and inherited
liquid assets; illiquid wealth can change with the consol price. The projection
also preserves aggregates of the steady household policies, the labor condition
and inherited illiquid wealth. Its columns are
orthonormal, and their coefficients describe signed deviations from the
steady distribution. basis.decode() checks positivity by default. There is
no clipping or renormalization. Linear simulations report negative
reconstruction mass explicitly; a good moment fit does not establish a valid
distribution approximation.
policy.simulate(innovations) solves the linear state recursion. Its aggregate
paths are log deviations, including log(1+inflation); they are not the
level-deviation paths of the nonlinear solver. policy.covariance() solves
the first-order stationary covariance equation. Changing shock_std changes
that covariance, but not the first-order policy coefficients or steady means.
That certainty equivalence is a property of the approximation order.
policy.audit() uses the full household grid and transports the actual
distribution on every held-out date. It solves consistency between next
distribution coefficients and household continuation values while retaining
the fitted aggregate policy. It reports market and goods residuals, budgets,
mass, grid exits, Euler/KKT errors and projection loss. Its distribution array
includes the final transported distribution, so it has one more row than its
paths. An audit can fail even when the projected Schur equations converge.
Euler/KKT errors include household interpolation error and are separate from
the aggregate acceptance threshold.
A solved stochastic terminal boundary
solve_hank_stochastic_tree optimizes households and clears markets at each
finite-history node. Each leaf has additional unknown distribution
coefficients. They must equal the projection of the distribution actually
transported beyond that leaf. The terminal policy then supplies successor
household marginal values, inflation and output, conditional on those
coefficients, the chosen nominal rate and the persistent disturbance.
Terminal Phillips expectations use two-point Gaussian quadrature. Expected household marginal values integrate the linear terminal rule exactly. The finite portion is nonlinear, while the terminal rule remains first order; nonlinear arithmetic at the boundary does not turn this into a second-order stochastic solution. Horizon, shock-size and compression comparisons remain necessary.
The default correction uses damped Newton–GMRES with centered products of the
full nonlinear residual and sparse preconditioning. The preconditioner
approximates local responses and asset carry; household expectations and
distribution effects remain in the outer residual. preconditioner="none"
provides a check. method="hybr" uses a dense finite-difference reference.
This is a separate continuation backend; the original
analytic finite-tree solver is unchanged.
converged requires finite-tree market and goods residuals, terminal
consistency, household budgets, conserved/nonnegative mass and negligible
policy-grid exits. Terminal projection loss, minimum reconstructed terminal
mass, Euler/KKT errors and upper-grid mass are reported separately. Small
finite-tree residuals do not certify the projected infinite tail. Iteration,
linear-probe and line-search failures retain their last feasible iterate and
history. Invalid initial domains raise; the dense comparison can also raise
if its unguarded MINPACK trial leaves the economic domain.
Liquid bonds and an illiquid real consol
from puremacro.models import calibrate_two_asset_equilibrium
two_ss = calibrate_two_asset_equilibrium(n_a=15, n_b=15, a_max=80.)
two_policy = solve_hank_stochastic_policy(two_ss, distribution_rank=16)
two_result = solve_hank_stochastic_tree(two_policy, tree)
print(two_result.paths[["liquid_mkt", "illiquid_mkt", "goods_mkt"]])
This closure has liquid nominal government bonds of fixed real quantity \(B\), and an illiquid real consol with fixed unit supply \(S\), price \(q_t\), and constant real coupon \(d\). The realized returns are
The existing two-asset EGM household chooses liquid and illiquid savings, paying its convex portfolio adjustment cost \(\chi\). Its budget is
The income process has the existing two productivity states. Wages, profits and taxes are distributed proportionally to productivity, whose mean is one. With labor-only production and common union hours \(N_t=Y_t\), flexible labor supply satisfies \(w_t\int e c^{-\gamma}\,dD_t=\varphi Y_t^{1/\nu}\). Rotemberg pricing and the Taylor rule complete the aggregate block.
Taxes are \(T_t=r^b_tB+dS\). Consol capital gains enter household returns, not government expenses. Total nonfinancial income is \(z_t=Y_t-\text{price adjustment cost}_t-T_t\). The solver clears both asset markets, \(\int b'\,dD_t=B\) and \(\int a'\,dD_t=q_tS\), alongside labor supply and the Phillips equation. It independently checks \(Y_t=C_t+\int\chi\,dD_t+\text{price adjustment cost}_t\).
Calibration takes household preferences, adjustment costs, steady returns
and nonfinancial income as inputs. Household demands determine \(B\) and
\(S\); \(q=1\), the coupon equals the steady illiquid return, and labor
disutility matches steady output/hours. This is a consol economy, not the
productive-capital closure of a published two-asset HANK replication. The
steady state's income is distinct from its aggregate wage w.
Numerical limits and evidence
One-asset policy derivatives are analytic. Two-asset policy derivatives use cached centered household differences and differentiated bilinear transport; tests compare their joint equations with independently evaluated nonlinear period equations. Both models check Schur determinacy, stability, conditioning and equation residuals. No pseudoinverse or arbitrary stable policy replaces a failed determinacy check.
The distribution is reduced, but all household marginal-value controls are
retained. Ordered QZ is dense. max_system_size defaults to 1,800 and checks
the dimension before basis construction; it is not an RSS bound. The tree's
max_workspace_bytes bounds an estimated working-array footprint, excluding
the already prepared policy, sparse-factor and Python/BLAS overhead. Large
household grids will need further value-function reduction or an iterative
stable-subspace solver.
In particular, the existing one-asset calibration's default 500 × 7 grid
exceeds this Schur limit; the examples deliberately use smaller grids.
Raising the limit permits a larger dense allocation without changing its
computational complexity.
tools/reference_validation/validate_hank_recursive.py records full and
compressed policies, held-out audits, horizon comparisons, dense/Krylov
agreement and two-asset grid sensitivity. Its frozen artifacts live under
tests/fixtures/hank_recursive/: source and artifact hashes, state/feedback
matrices, seeds and CSV histories permit independent recomputation. Failed
audits remain in the record. Grid-sensitivity runs hold household preference
and price inputs fixed while recalibrating asset supplies; they are not a
certification of a fixed-supply economy's grid convergence.
The retained run uses 80 asset points and 3 income states for one asset, and
12 × 12 asset points and 2 income states for two assets. Its rank-16 snapshot
bases were trained on 160 small innovations and evaluated on 48 independent
innovations with standard deviation 1e-4.
| Check | One asset | Two assets |
|---|---|---|
| Full → compressed distribution dimension | 236 → 16 | 285 → 16 |
| Full → compressed Schur dimension | 481 → 261 | 867 → 598 |
| Largest held-out linear aggregate gap | 4.44e-8 |
2.90e-10 |
| Full-policy nonlinear audit residual | 1.65e-5 (fails) |
8.52e-6 (passes) |
| Compressed-policy nonlinear audit residual | 2.74e-5 (fails) |
9.49e-6 (passes) |
| Compressed audit at one-quarter amplitude | 1.03e-6 (passes) |
5.33e-7 (passes) |
| Largest nonlinear tree residual, horizons 4/8/16/32 | 3.72e-10 |
5.54e-11 |
| Dense versus Krylov tree path gap | 2.07e-8 |
8.31e-10 |
The rollout audit threshold is 1e-5 in all audit rows. The one-asset failure also occurs
with the full constrained distribution, so a small linear compression gap
does not remove nonlinear approximation error. The two-asset result is near
the acceptance boundary; it is not a guarantee for other shock histories.
Audits use the previous quoted consol price when computing realized
returns, even when the approximate policy leaves an asset-market residual.
Maximum household Euler/KKT errors on these demonstration grids remain about
1e-3 and 3e-3, respectively. The tree convergence flag does not certify
those household-grid errors.
prior_variance_only/ retains the first compression experiment, its exact
source bytes and failed determinacy checks. Preserving market observables
before truncation resolves those failures in the reported rank-8/rank-16
two-asset cases; rank 4 is now rejected explicitly because it omits required
directions. Timing observations in the JSON are descriptive, not a repeated
performance benchmark.