- Author: Yuxuan Zhao
- Updated: 2026-08-23
This notebook solves a two-asset Heterogeneous-Agent New Keynesian model based on the economic environment of Kaplan, Moll, and Violante (2018).
1) Model
Time is quarterly. A unit mass of households faces idiosyncratic labor-income risk and trades a low-return liquid asset $b$ and a high-return illiquid asset $a$. The illiquid account is subject to a transaction cost. Aggregate production uses capital and labor, prices are sticky, monetary policy follows a Taylor rule, and fiscal transfers close the government budget.
The aggregate exogenous paths are
$$ E=(\log Z,\eta), $$
and the sequence-space unknown paths are
$$ U=(\pi,w,Y,K,q). $$
For a horizon $T$, each symbol denotes the complete path from date $0$ through $T-1$. The transition begins from the deterministic steady-state household distribution and inherited capital, equity, and nominal interest rate. Beyond $T-1$, continuation values and aggregate prices return to steady state.
1.1 Household state, timing, and preferences
At the beginning of date $t$, a household is in state
$$ s_t=(e_t,b_t,a_t), $$
where $e_t$ is labor efficiency, $b_t$ is current liquid wealth, and $a_t\geq0$ is current illiquid wealth. It chooses
$$ c_t,\qquad n_t,\qquad b_{t+1},\qquad a_{t+1}. $$
Preferences are
$$ \mathbb E_0\sum_{t=0}^{\infty}\beta^t \left[ \frac{c_t^{1-\gamma}-1}{1-\gamma} -\varphi\frac{n_t^{1+1/\nu}}{1+1/\nu} \right], $$
with the log-utility limit at $\gamma=1$. Idiosyncratic efficiency follows the finite Markov chain
$$ \Pr(e_{t+1}=e'\mid e_t=e)=P_{ee'}. $$
This earnings transition is the only exogenous movement across household states. There is no death state.
1.2 Liquid and illiquid asset laws
Let $R_t^b$ and $R_t^a$ be gross real returns. Borrowers pay the wedge $\kappa_b$:
$$ R_t^b(b_t)=R_t^b+\kappa_b\mathbf 1\{b_t<0\}. $$
Define the active quarterly deposit into the illiquid account as
$$ D_t=a_{t+1}-R_t^aa_t. $$
For a quarterly period of length $\Delta=1/4$, the transaction cost is
$$ \Psi_\Delta(D_t,a_t) =\chi_0|D_t| +\frac{\chi_1}{\chi_2}\Delta^{1-\chi_2} \left|\frac{D_t}{\max(a_t,\underline a)}\right|^{\chi_2} \max(a_t,\underline a). $$
The period budget constraint is
$$ c_t+b_{t+1}+a_{t+1}+\Psi_t =R_t^b(b_t)b_t+R_t^aa_t+(1-\tau_t)w_te_tn_t+T_t, $$
subject to
$$ b_{t+1}\geq\underline b,\qquad a_{t+1}\geq0, \qquad c_t>0,\qquad 0\leq n_t\leq1. $$
Only the absolute value at the transaction-cost kink is smoothed numerically: $|D|_\epsilon=\sqrt{D^2+\epsilon^2}$.
1.3 Recursive household problem
Given an aggregate path, the household solves
$$ V_t(e,b,a)=\max_{c,n,b',a'} \left\{ u(c,n)+\beta\sum_{e'}P_{ee'}V_{t+1}(e',b',a') \right\} $$
subject to the budget and portfolio constraints above. There is no survival factor multiplying continuation value. The distribution evolves only through the policy functions and the earnings transition:
$$ D_{t+1}=\mathcal T(g_t^b,g_t^a,P)D_t. $$
Away from constraints and the transaction-cost kink, the liquid Euler equation is
$$ u_c(c_t,n_t)=\beta\mathbb E_t \left[R_{t+1}^b(b_{t+1})u_c(c_{t+1},n_{t+1})\right]. $$
The illiquid first-order condition is
$$ \frac{W_t^a(e,b',a')}{W_t^b(e,b',a')} =1+\Psi_1(a',a;R_t^a), \qquad W_t^j=\beta\mathbb E_t[V_{j,t+1}]. $$
At $b'=\underline b$, a Kuhn-Tucker multiplier defines a separate constrained branch.
1.4 Competitive final-good producer
A competitive final-good producer combines a continuum of differentiated intermediate goods with the CES technology
$$ Y_t=\left[\int_0^1 y_{j,t}^{\frac{\epsilon-1}{\epsilon}}dj \right]^{\frac{\epsilon}{\epsilon-1}}, \qquad \epsilon>1. $$
Taking the final-good price $P_t$ as the numeraire, the producer minimizes the cost of delivering $Y_t$:
$$ \min_{\{y_{j,t}\}}\int_0^1p_{j,t}y_{j,t}dj \quad\text{s.t. the CES aggregator above}. $$
The first-order conditions give the price index and the demand curve facing intermediate producer $j$:
$$ P_t=\left[\int_0^1p_{j,t}^{1-\epsilon}dj\right]^{\frac{1}{1-\epsilon}}, \qquad y_{j,t}=\left(\frac{p_{j,t}}{P_t}\right)^{-\epsilon}Y_t. $$
Thus each intermediate firm has market power: changing its relative price $p_{j,t}/P_t$ changes the quantity demanded of its variety.
1.5 Intermediate firms: technology, factor demand, and current profit
Firm $j$ uses capital installed at the end of the previous quarter and current labor to produce
$$ y_{j,t}=Z_tK_{j,t-1}^{\alpha}N_{j,t}^{1-\alpha}. $$
In a symmetric equilibrium, all firms choose the same price and input bundle. Price dispersion therefore vanishes at the expansion point and aggregate production is
$$ Y_t=Z_tK_{t-1}^{\alpha}(N_t^f)^{1-\alpha}, \qquad N_t^f=\left(\frac{Y_t}{Z_tK_{t-1}^{\alpha}}\right)^{\frac{1}{1-\alpha}}. $$
Let $mc_t$ denote real marginal cost. Conditional factor demand gives
$$ w_t=(1-\alpha)mc_t\frac{Y_t}{N_t^f}, \qquad r_t^k=\alpha mc_t\frac{Y_t}{K_{t-1}}. $$
Equivalently, the macro block recovers marginal cost from candidate paths as
$$ mc_t=\frac{w_tN_t^f}{(1-\alpha)Y_t}. $$
Firm $j$ pays a Rotemberg resource cost when it changes its price. Its current real profit is
$$ \mathcal D_{j,t}=\left[ \left(\frac{p_{j,t}}{P_t}\right)^{1-\epsilon} -mc_t\left(\frac{p_{j,t}}{P_t}\right)^{-\epsilon} -\frac{\theta_p}{2} \left(\frac{p_{j,t}}{p_{j,t-1}}-1\right)^2 \right]Y_t. $$
The three terms are revenue, variable production cost, and the real resource cost of changing the price. Define net inflation by $\pi_t=P_t/P_{t-1}-1$. Under symmetry, aggregate adjustment costs and current profit become
$$ \Theta_t^p=\frac{\theta_p}{2}\pi_t^2Y_t, \qquad \mathcal D_t=(1-mc_t)Y_t-\Theta_t^p. $$
These adjustment costs are not transfers between agents: they absorb final goods and therefore also appear in goods-market clearing.
1.6 Dynamic price setting and the New Keynesian Phillips curve
The intermediate firm inherits $p_{j,t-1}$ and chooses $p_{j,t}$ while taking the complete aggregate transition path as given. Its recursive pricing problem is
$$ \mathcal W_t(p_{j,t-1}) =\max_{p_{j,t}}\left\{\mathcal D_{j,t} +\beta\mathcal W_{t+1}(p_{j,t})\right\}, \qquad \mathcal W_T=\mathcal W_{ss}. $$
The chosen price affects current demand and current adjustment costs. It also becomes next quarter's inherited price, so the pricing condition is forward looking. Differentiating the problem, applying the envelope theorem, and only then imposing symmetry gives the nonlinear Rotemberg condition
$$ 0=(1-\epsilon)+\epsilon mc_t -\theta_p\pi_t(1+\pi_t) +\beta\frac{Y_{t+1}}{Y_t} \theta_p\pi_{t+1}(1+\pi_{t+1}). $$
Around the zero-inflation steady state, the code uses the following stable local discrete-time representation of that first-order condition:
$$ \boxed{ H_t^{NKPC} =\log(1+\pi_t) -\beta\frac{Y_{t+1}}{Y_t}\log(1+\pi_{t+1}) -\kappa_p(mc_t-\bar{mc})=0. } $$
Here $\bar{mc}$ is steady-state marginal cost and $\kappa_p$ is the local Phillips-curve slope. Higher marginal cost makes firms want to raise prices, whereas a larger Rotemberg cost makes inflation respond more slowly. At the terminal date, $Y_T=\bar Y$ and $\pi_T=0$.
1.7 Capital accumulation, the illiquid return, and equity valuation
One unit of the final good can be converted into one unit of installed capital. Capital therefore evolves according to
$$ K_t=(1-\delta)K_{t-1}+I_t, \qquad I_t=K_t-(1-\delta)K_{t-1}. $$
Capital carried into date $t$ pays its rental income plus the undepreciated capital stock. Its gross return is
$$ R_t^a=1+r_t^k-\delta. $$
The household's illiquid account contains both physical capital and a unit supply of firm equity. Let $q_{t-1}$ be the beginning-of-period market value of equity. During date $t$, equity pays current profit $\mathcal D_t$ and then trades ex dividend at $q_t$. Its realized gross return is therefore
$$ R_t^q=\frac{\mathcal D_t+q_t}{q_{t-1}}. $$
Because capital and equity are held inside the same illiquid account, no arbitrage requires $R_t^q=R_t^a$. The corresponding residual is
$$ \boxed{ H_t^{equity} =R_t^aq_{t-1}-\mathcal D_t-q_t=0. } $$
Thus $H^{equity}$ is not another market-clearing condition. It is the dynamic asset-pricing condition that makes equity deliver the same return as physical capital. Equivalently, it recursively values equity as the discounted stream of future profits.
1.8 Monetary and fiscal policy
The nominal policy rate follows
$$ i_t-\bar i =\rho_i(i_{t-1}-\bar i)+(1-\rho_i)\phi_\pi\pi_t+\eta_t. $$
A liquid bond purchased at date $t-1$ pays $1+i_{t-1}$ nominal units at date $t$. Hence its realized gross real return is
$$ R_t^b=\frac{1+i_{t-1}}{1+\pi_t}. $$
This timing is important: the current monetary innovation changes $i_t$ and therefore directly enters $R_{t+1}^b$, while current inflation revalues the nominal payoff inherited from the previous quarter.
Government debt $\bar B$, purchases $G$, and the labor-income tax rate $\tau$ are fixed. Lump-sum transfers balance the period government budget:
$$ T_t=\tau w_tN_t^f-G-(R_t^b-1)\bar B. $$
A monetary expansion can therefore affect households indirectly through both labor income and the fiscal savings generated by lower debt-service costs.
1.9 Recursive equilibrium, market clearing, and residuals
Given the beginning-of-date distribution $D_t(e,b,a)$ and household policies, define
$$ \begin{aligned} B_t^h&=\int g_t^b(e,b,a)\,dD_t, &A_t^h&=\int g_t^a(e,b,a)\,dD_t,\\ C_t&=\int c_t(e,b,a)\,dD_t, &N_t^h&=\int e n_t(e,b,a)\,dD_t. \end{aligned} $$
$B_t^h$ and $A_t^h$ are the end-of-date positions $b_{t+1}$ and $a_{t+1}$ chosen at date $t$; the code indexes them by their decision date. The three market-clearing conditions are
$$ \boxed{ B_t^h=\bar B, \qquad A_t^h=K_t+q_t, \qquad N_t^h=N_t^f. } $$
The first equation clears the liquid government-bond market. The second clears the illiquid asset market: household illiquid wealth finances installed capital and the market value of the unit equity claim. The third equates efficiency units of labor supplied by households with labor demanded by firms.
For the unknown paths $U=(\pi,w,Y,K,q)$ and exogenous paths $E=(\log Z,\eta)$, the outer solver uses exactly five residuals:
$$ \begin{aligned} H_t^{NKPC}&=\log(1+\pi_t) -\beta\frac{Y_{t+1}}{Y_t}\log(1+\pi_{t+1}) -\kappa_p(mc_t-\bar{mc}),\\ H_t^B&=B_t^h-\bar B,\\ H_t^A&=A_t^h-K_t-q_t,\\ H_t^N&=N_t^h-N_t^f,\\ H_t^{equity}&=R_t^aq_{t-1}-\mathcal D_t-q_t. \end{aligned} $$
Stacking all dates by residual block gives
$$ H(U;E)= \begin{bmatrix} H^{NKPC}\\H^B\\H^A\\H^N\\H^{equity} \end{bmatrix} \in\mathbb R^{5T}, \qquad H(U;E)=0. $$
Finally, define aggregate household portfolio costs and borrowing-service costs as
$$ \Chi_t=\int\Psi_t\,dD_t, \qquad \mathcal S_t^b=\kappa_b\int[-b]_+\,dD_t. $$
Goods-market clearing is
$$ H_t^G =Y_t-C_t-I_t-G-\Theta_t^p-\Chi_t-\mathcal S_t^b=0. $$
It is retained as an independent Walras-law diagnostic rather than a sixth solver target. When household budgets, the government budget, the three markets, and equity pricing are all implemented consistently, $H_t^G$ must also be zero.
2) Julia Setup
The setup cell locates the repository root, activates the pinned Julia project,
loads plotting and linear-algebra tools, and includes src/SSJ_Function.jl.
All economic and numerical functions remain in the source file; the notebook
only defines the model, sets parameters, calls blocks, and displays results.
using Pkg
project_root = isfile(joinpath(pwd(), "Project.toml")) ? pwd() : normpath(joinpath(pwd(), ".."))
Pkg.activate(project_root)
using LinearAlgebra, Plots, Printf
include(joinpath(project_root, "src", "SSJ_Function.jl"))
using .KMVDiscrete
default(size=(680, 380), linewidth=2, legend=:best)
3) Transition Blocks and DAG
For a finite horizon $T$, stack the unknown and exogenous sequences as
$$ U=(\pi,w,Y,K,q)\in\mathbb R^{5T}, \qquad E=(\log Z,\eta)\in\mathbb R^{2T}. $$
For a given $E$, the outer solver proposes $U$. One evaluation of the general- equilibrium map then passes through five blocks. The first four construct the paths needed by households and firms; the final block evaluates every equilibrium condition. In particular, the NKPC and the equity-pricing equation belong to the residual block rather than to separate computational blocks.
3.1 Monetary and Fisher block
Given the inflation path and the monetary innovation, this block recursively constructs the nominal policy rate and then converts it into the realized gross return on a one-period liquid bond:
$$ i_t=\bar i+\rho_i(i_{t-1}-\bar i)+(1-\rho_i)\phi_\pi\pi_t+\eta_t, \qquad R_t^b=\frac{1+i_{t-1}}{1+\pi_t}. $$
Input: $(\pi,\eta)$ and $i_{-1}=\bar i$.
Output: $(i,R^b)$.
This is a forward recursion followed by an algebraic Fisher mapping; it does not require a root solve. The lag in the Fisher equation preserves the timing of the one-period bond bought in the preceding quarter.
3.2 Fiscal block
The government budget determines the lump-sum transfer path after taxes, purchases, and interest payments on government debt have been paid:
$$ T_t=\tau w_tN_t^f-G-(R_t^b-1)\bar B. $$
Input: $(w,N^f,R^b)$ and fixed $(\tau,G,\bar B)$.
Output: $T$.
This block is static conditional on its input paths. Keeping it explicit makes the fiscal interest-saving channel transparent: a change in $R^b$ changes debt service and therefore household transfers.
3.3 Firm block
Conditional on the candidate paths $(Y,K,w,\pi)$ and TFP, the firm block inverts the production function to obtain labor demand and then computes the factor prices, investment, price-adjustment cost, and dividend flow implied by those paths. With $Z_t=\bar Z\exp(\log Z_t)$ and inherited $K_{t-1}$,
$$ N_t^f=\left(\frac{Y_t}{Z_tK_{t-1}^{\alpha}}\right)^{1/(1-\alpha)}, \qquad mc_t=\frac{w_tN_t^f}{(1-\alpha)Y_t}, $$
$$ r_t^k=\frac{\alpha mc_tY_t}{K_{t-1}}, \qquad R_t^a=1+r_t^k-\delta, \qquad I_t=K_t-(1-\delta)K_{t-1}, $$
$$ \Theta_t^p=\frac{\theta_p}{2}\pi_t^2Y_t, \qquad \mathcal D_t=(1-mc_t)Y_t-\Theta_t^p. $$
Input: $(Y,K,w,\pi,\log Z)$ and $K_{-1}=\bar K$.
Output: $(Z,N^f,mc,r^k,R^a,I,\Theta^p,\mathcal D)$.
This block does not impose optimal price setting or equity pricing. It only computes firm quantities and cash flows conditional on the proposed paths; the NKPC and equity-pricing equations are checked together in the residual block below.
3.4 Two-asset household transition block
Given complete price and transfer paths, the block solves the recursive household problem
$$ V_t(e,b,a)=\max_{c,n,b',a'}\left\{u(c,n)+\beta \sum_{e'}P_{ee'}V_{t+1}(e',b',a')\right\}, $$
subject to the two-asset budget constraint and borrowing constraints in Section 1. Starting from $(V_{b,T},V_{a,T})=(V_{b,ss},V_{a,ss})$, backward two-asset EGM constructs $(c_t,n_t,b'_{t},a'_{t})$ at every date. Starting from $D_0=D_{ss}$, the induced transition lotteries and the earnings matrix propagate the distribution:
$$ D_{t+1}=\mathcal T(g_t^b,g_t^a,P)D_t. $$
The date-$t$ policies are then integrated against $D_t$.
Input: $(R^b,R^a,w,\tau,T)$, terminal marginal values, and $D_0$.
Output: $(B,A,C,H,N,\Chi)$ and the terminal distribution $D_T$.
Thus the household block is one sequence-to-sequence transition map, not a collection of independent static choices. No survival probability, newborn distribution, or annuity-return adjustment enters this discrete-time version.
3.5 Equilibrium residual block
This final block takes the proposed aggregate paths and the outputs of the firm and household blocks and asks whether they constitute an equilibrium. It evaluates the dynamic pricing equation, three market-clearing conditions, and the equity-pricing equation:
$$ H_t^{NKPC}=\log(1+\pi_t)-\beta\frac{Y_{t+1}}{Y_t} \log(1+\pi_{t+1})-\kappa_p(mc_t-\bar{mc}), $$
$$ H_t^B=B_t-\bar B, \qquad H_t^A=A_t-K_t-q_t, \qquad H_t^N=N_t-N_t^f, $$
$$ H_t^{equity}=R_t^aq_{t-1}-(\mathcal D_t+q_t). $$
Input: $(\pi,Y,K,q)$, firm outputs, and household aggregates.
Output:
$$ H(U;E)=\left[H^{NKPC};H^B;H^A;H^N;H^{equity}\right] \in\mathbb R^{5T}. $$
The outer solver changes $U=(\pi,w,Y,K,q)$ until $H(U;E)=0$. The goods-market residual
$$ H_t^G=Y_t-C_t-I_t-G-\Theta_t^p-\Chi_t-\mathcal S_t^b $$
is retained as an independent Walras-law diagnostic, not as a sixth target.
Initial and terminal conditions
The transition starts from
$$ D_0=D_{ss},\qquad K_{-1}=K_{ss},\qquad q_{-1}=q_{ss}, \qquad i_{-1}=i_{ss}. $$
Household continuation marginal values and the NKPC terminal leads are evaluated at the deterministic steady state. No restriction is imposed on $D_T$.
The table below is the compact interface specification; the diagram visualizes the same dependency structure.
block_interfaces = kmv_block_table()
block_interfaces
display(Base.HTML(kmv_discrete_dag_svg()))
4) Calibration and State Grids
This reorganization leaves the existing provisional calibration unchanged. The parameters combine KMV-style preferences and transaction-cost curvature with a quarterly discrete earnings approximation and a no-death steady state. They should be interpreted as a computational baseline, not as a converted replication of KMV's continuous-time calibration.
The household state grid is three-dimensional:
$$ (e,b,a)\in\mathcal E\times\mathcal B\times\mathcal A. $$
The liquid grid contains the borrowing limit and an explicit zero, and is dense near both. The illiquid grid is dense near $a=0$, where transaction-cost and portfolio policies are most nonlinear. The multiplier grid is used to solve the borrowing-constrained branch.
params = KMVDiscreteParams(
beta=0.98132, gamma=1.0, frisch=1.0, phi_n=2.0, tau=0.30,
chi0=0.0438, chi1=0.30, chi2=1.402, period_years=0.25,
borrowing_wedge=0.015, bmin=-0.65, bmax=50.0, amax=80.0,
nB=25, nA=31, nE=5, nKappa=25, rho_e=0.966, sigma_e=0.92
)
macro_params = KMVMacroParams(Y=1.0, delta=0.02, theta_p=100.0,
kappa_p=0.08, phi_pi=1.5, rho_i=0.0)
grids = make_kmv_grids(params)
(liquid_points=length(grids.b), illiquid_points=length(grids.a),
earnings_states=length(grids.e), mass=sum(grids.ed))
p1 = plot(grids.b, marker=:circle, xlabel="grid index", ylabel="b", title="Liquid grid", legend=false)
p2 = plot(grids.a, marker=:circle, xlabel="grid index", ylabel="a", title="Illiquid grid", legend=false)
plot(p1, p2, layout=(1,2), size=(1250,500), dpi=120,
left_margin=10Plots.mm, right_margin=6Plots.mm,
bottom_margin=15Plots.mm, top_margin=7Plots.mm,
guidefontsize=12, tickfontsize=10, titlefontsize=14)
5) Solve the Household Problem by Two-Asset EGM
The household is the heterogeneous block. src/SSJ_Function.jl implements its
backward and forward operations; this section states exactly what those
functions compute.
5.1 Backward continuation objects
Given next-period marginal values $(V_{b,t+1},V_{a,t+1})$, form
$$ W_t^b(e,b',a')=\beta\sum_{e'}P_{ee'}V_{b,t+1}(e',b',a'), $$
$$ W_t^a(e,b',a')=\beta\sum_{e'}P_{ee'}V_{a,t+1}(e',b',a'). $$
For each current $(e,a)$ and proposed liquid choice $b'$, solve the illiquid first-order condition
$$ \frac{W_t^a(e,b',a')}{W_t^b(e,b',a')} =1+\Psi_1(a',a;R_t^a) $$
for $a'$. Conditional on $(b',a')$, the liquid Euler equality gives
$$ c_t=(W_t^b)^{-1/\gamma}, $$
and the intratemporal labor condition determines $n_t$.
5.2 Endogenous current-liquid grid
Substitute $(c,n,b',a')$ into the budget constraint and invert the current liquid return schedule. This produces the endogenous current-liquid position
$$ b^{endo}(e,b',a). $$
Interpolation maps $(b',a')$ from the endogenous grid back to the fixed $b$ grid for each $(e,a)$.
5.3 Borrowing-constrained branch
When the unconstrained solution implies $b'<\underline b$, set
$$ b'=\underline b $$
and solve the illiquid condition jointly with a nonnegative Kuhn-Tucker multiplier. The constrained and unconstrained branches are spliced at the endogenous cutoff. Policies are clipped only at the numerical upper bounds.
5.4 Stationary backward and forward fixed points
At stationary prices, iterate the backward map until $(V_b,V_a)$ converge. Holding the converged policies fixed, iterate
$$ D'=\mathcal T(g^b,g^a,P)D $$
until the invariant distribution converges. Bilinear lottery weights on the $(b,a)$ grids preserve probability mass. Aggregation then gives
$$ (B,A,C,H,N,\Chi). $$
The complete stationary household interface is therefore
$$ (R^b,R^a,w,\tau,T;\text{grids, parameters}) \longmapsto (g^b,g^a,c,n,V_b,V_a,D,B,A,C,H,N,\Chi). $$
6) Deterministic Steady State
The baseline call first solves the stationary household problem at the supplied steady prices. The macro closure then selects production objects so that capital and equity earn the common illiquid return and
$$ A=K+q. $$
The steady state is the expansion point for every household and general- equilibrium sequence-space derivative below.
ss = calibrate_kmv_steady_state(params, macro_params;
Rb=1.005, Ra=1.01425, w=1.05, T=0.10,
backward_tol=6e-6, forward_tol=5e-9, damp=0.8)
h = ss.household
m = ss.macro_ss
(backward_iterations=h.backward_iterations, backward_error=h.backward_error,
forward_iterations=h.forward_iterations, forward_error=h.forward_error)
steady_moments = [
(moment="liquid wealth B", value=h.B),
(moment="illiquid wealth A", value=h.A),
(moment="consumption C", value=h.C),
(moment="mean hours H", value=h.H),
(moment="efficiency labor N", value=h.N),
(moment="share b < 0", value=h.negative_b),
(moment="capital K", value=m.K),
(moment="equity q", value=m.q),
(moment="capital share alpha", value=m.alpha),
(moment="marginal cost mc", value=m.mc),
(moment="equity residual", value=m.equity_residual),
(moment="goods residual", value=m.goods_residual)
]
steady_moments
calibration_comparison = [
(moment="liquid wealth", model=h.B, approximate_target=1.04, gap=h.B-1.04),
(moment="illiquid wealth", model=h.A, approximate_target=11.68, gap=h.A-11.68),
(moment="mean hours", model=h.H, approximate_target=0.50, gap=h.H-0.50)
]
calibration_comparison
6.3 Household policy functions
All horizontal axes are current state variables.
- Consumption: $c_t(b_t\mid e_t,a_t)$ over the low and middle liquid-wealth region.
- Liquid policy: $b_{t+1}(b_t\mid e_t,a_t)$ for several current illiquid positions. The 45-degree line identifies an unchanged liquid stock.
- Illiquid policy: $a_{t+1}(b_t\mid e_t,a_t)$ for the same current illiquid positions. Its horizontal axis is current liquid wealth so that the portfolio adjustment kink near $b_t=0$ is visible.
The third panel has no 45-degree line because its horizontal and vertical axes are different assets. Active illiquid deposits are
$$ D_t=a_{t+1}-R^aa_t. $$
Plotting $a_{t+1}$ displays the actual state transition; subtracting $R^aa_t$ would magnify the transaction-cost kink. The liquid panel focuses on $b_t\in[\underline b,5]$, while the illiquid panel extends to $b_t\in[\underline b,15]$.
iz_low, iz_mid, iz_high = 1, cld(length(grids.e),2), length(grids.e)
ia_zero = 1
ia_one = argmin(abs.(grids.a .- 1.0))
current_e = round(grids.e[iz_mid], digits=3)
b_plot_consumption = findall(grids.b .<= 15.0)
b_plot_liquid = findall(grids.b .<= 5.0)
b_plot_illiquid = findall(grids.b .<= 15.0)
p_c = plot(grids.b[b_plot_consumption], h.c[iz_low,b_plot_consumption,ia_zero], label="low current e, current a=0", xlabel="current liquid wealth bₜ", ylabel="current consumption cₜ", title="Consumption policies")
plot!(p_c, grids.b[b_plot_consumption], h.c[iz_mid,b_plot_consumption,ia_zero], label="middle current e, current a=0")
plot!(p_c, grids.b[b_plot_consumption], h.c[iz_mid,b_plot_consumption,ia_one], label="middle current e, current a≈1", linestyle=:dash)
plot!(p_c, grids.b[b_plot_consumption], h.c[iz_high,b_plot_consumption,ia_one], label="high current e, current a≈1", linestyle=:dot)
a_targets = [0.0, 0.5, 1.0, 2.0, 5.0]
a_indices = unique([argmin(abs.(grids.a .- value)) for value in a_targets])
p_bp = plot(xlabel="current liquid wealth bₜ", ylabel="choice bₜ₊₁", title="Liquid policy (current eₜ=$(current_e))", xlims=(params.bmin,5), left_margin=14Plots.mm)
for ia in a_indices
plot!(p_bp, grids.b[b_plot_liquid], h.bp[iz_mid,b_plot_liquid,ia], label="current aₜ=$(round(grids.a[ia],digits=2))")
end
plot!(p_bp, [params.bmin,5.0], [params.bmin,5.0], color=:black, linewidth=1.5, linestyle=:dash, label="45°: bₜ₊₁=bₜ")
p_ap = plot(xlabel="current liquid wealth bₜ", ylabel="choice aₜ₊₁", title="Illiquid policy (current eₜ=$(current_e))", xlims=(params.bmin,15), left_margin=14Plots.mm)
for ia in a_indices
plot!(p_ap, grids.b[b_plot_illiquid], h.ap[iz_mid,b_plot_illiquid,ia], label="current aₜ=$(round(grids.a[ia],digits=2))")
end
plot(p_c, p_bp, p_ap, layout=(1,3), size=(2050,560), dpi=120,
guidefontsize=11, tickfontsize=10, legendfontsize=9, titlefontsize=14, framestyle=:box)
marginal_b = vec(sum(h.D, dims=(1,3)))
marginal_a = vec(sum(h.D, dims=(1,2)))
p_db = bar(grids.b, marginal_b, xlabel="b", ylabel="mass", title="Liquid-wealth distribution", legend=false)
p_da = bar(grids.a, marginal_a, xlabel="a", ylabel="mass", title="Illiquid-wealth distribution", legend=false)
plot(p_db, p_da, layout=(1,2), size=(900,340))
7) Transition-Block Implementation
For a candidate household-input path
$$ X=(R^b,R^a,w,T,\tau), $$
the household transition block performs two passes.
7.1 Backward policy pass
Start from the stationary terminal marginal values
$$ (V_{b,T},V_{a,T})=(V_{b,ss},V_{a,ss}) $$
and apply the two-asset EGM step for $t=T-1,\ldots,0$. This produces
$$ \{b_{t+1}(e,b,a),a_{t+1}(e,b,a),c_t(e,b,a),n_t(e,b,a)\}_{t=0}^{T-1}. $$
7.2 Forward distribution pass
Start from $D_0=D_{ss}$. At each date, aggregate current policies under $D_t$ and then transport mass using the policy lotteries and earnings matrix:
$$ D_{t+1}=\mathcal T(g_t^b,g_t^a,P)D_t. $$
The output paths are
$$ (B,A,C,H,N,\Chi), $$
together with $D_T$. The terminal distribution is an output and is not forced to equal $D_{ss}$.
7.3 Reduced equilibrium map
The full transition evaluator composes
$$ (U,E) \longmapsto(R^b,R^a,w,T,\tau) \longmapsto(B,A,C,N,\Chi) \longmapsto H(U;E). $$
This is the nonlinear map that is differentiated by SSJ.
8) Household Partial-Equilibrium Jacobians
The household sequence-space mapping is
$$ (R^b,R^a,w,T,\tau)\longmapsto(B,A,C,N,\Chi), $$
with blocks
$$ J_{t,s}^{o,x}=\frac{\partial o_t}{\partial x_s}. $$
We now construct these blocks with the two-asset Fake News Algorithm. For each input $x$, a single perturbation at the end of a stationary path and one backward pass generate the policy response to news at every horizon. Bilinear lotteries on $(b,a)$ give the one-step distribution news. Steady-state expectation vectors propagate that news forward, and diagonal recursion recovers the full Jacobian. Hence the expensive operation scales with the number of horizons rather than rerunning a complete transition for every dated input.
A short $T=6$ brute-force calculation remains below solely as a numerical validation. The production Jacobian uses $T_J=120$.
T_check = 6
naive_check_seconds = @elapsed naive_check = kmv_household_jacobian(ss, T_check; h=2e-5)
fake_check_seconds = @elapsed fake_check = kmv_household_fake_news_jacobian(ss, T_check; h=2e-5)
fake_news_error = maximum(abs.(naive_check.J .- fake_check.J))
T_J = 120
hhJ_seconds = @elapsed hhJ = kmv_household_fake_news_jacobian(ss, T_J; h=2e-5)
(method=hhJ.method, size=size(hhJ.J), seconds=hhJ_seconds,
short_horizon_error=fake_news_error,
short_naive_seconds=naive_check_seconds, short_fake_seconds=fake_check_seconds,
finite=all(isfinite, hhJ.J))
J_C_Rb = hhJ.J[2*T_J+1:3*T_J, 1:T_J]
direct_rate_path = -0.0025 .* 0.6 .^ (0:T_J-1)
direct_C = J_C_Rb * direct_rate_path
direct_plot_horizon = 40
direct_periods = 0:direct_plot_horizon-1
plot(direct_periods, 100 .* direct_C[1:direct_plot_horizon] ./ h.C, xlabel="quarters",
ylabel="percent of steady C", title="Direct consumption response", legend=false)
9) Compose the General-Equilibrium Jacobian
Let $X=(R^b,R^a,w,T,\tau)$ collect household inputs and let $O=(B,A,C,N,\Chi)$ collect household outputs. The macro blocks provide $X_U$ and $X_E$, while Fake News provides $J^{O,X}$. The household contribution to equilibrium derivatives is therefore
$$ J^{O,U}=J^{O,X}X_U,\qquad J^{O,E}=J^{O,X}X_E. $$
Adding the direct NKPC, capital, labor-demand, and equity derivatives gives
$$ dH=H_UdU+H_EdE=0,\qquad G^{U,E}=-H_U^{-1}H_E. $$
solve_kmv_ge_jacobian now performs this block composition and reuses hhJ;
it no longer finite-differences the complete household transition for every
column of $H_U$ and $H_E$. The old full finite-difference routine remains in
the function file under solve_kmv_ge_jacobian_bruteforce for short validation
only.
T_GE = 120
ge_seconds = @elapsed ge = solve_kmv_ge_jacobian(
ss, T_GE; household_J=hhJ, hU=2e-5, hE=2e-5
)
(method=ge.method, seconds=ge_seconds, HU=size(ge.HU), HE=size(ge.HE),
steady_residual=maximum(abs.(ge.steady_residual)),
condition_number=cond(ge.HU))
10) Linear Monetary-Policy Impulse Responses
Consider an expansionary 25-basis-point innovation to the Taylor rule,
$$ d\eta_t=-0.0025\times0.6^t. $$
The GE Jacobian gives $dU=G^{U,E}dE$. The transition evaluator then maps the
solved unknown paths into household and macro responses. Following the display
convention in HANK.ipynb, the model is solved for 120 quarters and the figure
displays the first 40. This pushes the finite-horizon terminal condition well
beyond the plotted window. Responses are drawn as continuous lines without
point markers. Output and consumption are percent deviations from steady state;
inflation and the real liquid rate are annualized percentage points.
irf = kmv_monetary_irf(ss, ge; shock=-0.0025, rho=0.6)
irf_plot_horizon = 40
quarters = 0:irf_plot_horizon-1
shown = 1:irf_plot_horizon
pY = plot(quarters, 100 .* irf.Y[shown] ./ m.Y, title="Output", ylabel="percent", legend=false)
pC = plot(quarters, 100 .* irf.C[shown] ./ h.C, title="Consumption", ylabel="percent", legend=false)
pR = plot(quarters, 400 .* irf.Rb[shown], title="Real liquid rate", ylabel="annualized pp", legend=false)
pPi = plot(quarters, 400 .* irf.pi[shown], title="Inflation", ylabel="annualized pp", legend=false)
plot(pY, pC, pR, pPi, layout=(2,2), size=(900,620), xlabel="quarters")
11) Numerical and Economic Checks
A smooth policy plot is not a correctness test. This section checks the household first-order conditions, grid robustness, probability accounting, and aggregate equilibrium independently of the objects used as solver targets.
11.1 Budget, Euler, and Kuhn-Tucker residuals
For interior liquid choices, the code checks
$$ u_c=\beta E[V_b']. $$
For interior illiquid choices, it checks
$$ u_c(1+\Psi_1)=\beta E[V_a']. $$
At the liquid borrowing limit, it checks the corresponding inequality. Euler errors are reported as distribution-weighted log residuals; the budget error is reported in levels.
policy_checks = kmv_household_residuals(ss)
pc = policy_checks.summary
println("11.1 Household optimality and budget diagnostics")
println(repeat("-", 100))
@printf("%-42s %14s %18s %10s\n", "diagnostic", "value", "reference tolerance", "status")
println(repeat("-", 100))
residual_rows = [
("Maximum budget error", pc.maximum_budget_error, 1e-10, :less),
("Liquid Euler weighted RMSE", pc.liquid_euler_weighted_rmse, 5e-3, :less),
("Liquid Euler maximum", pc.liquid_euler_maximum, 5e-2, :less),
("Liquid KKT maximum violation", pc.liquid_kkt_maximum, 5e-2, :less),
("Illiquid Euler weighted RMSE", pc.illiquid_euler_weighted_rmse, 5e-3, :less),
("Illiquid Euler maximum", pc.illiquid_euler_maximum, 5e-2, :less),
("Mass at top liquid-policy boundary", pc.top_liquid_policy_mass, 1e-2, :less),
("Mass at top illiquid-policy boundary", pc.top_illiquid_policy_mass, 1e-2, :less),
]
for (label, value, tolerance, _) in residual_rows
@printf("%-42s %14.6e %18.1e %10s\n",
label, value, tolerance, value < tolerance ? "PASS" : "CHECK")
end
println(repeat("-", 100))
@printf("%-42s %13.3f%%\n", "Distribution mass with interior liquid choice", 100 * pc.liquid_interior_mass)
@printf("%-42s %13.3f%%\n", "Distribution mass with interior illiquid choice", 100 * pc.illiquid_interior_mass)
println("RMSE values are stationary-distribution weighted; maxima are worst-state diagnostics.")
checks = (
jacobian_method=ge.method,
fake_news_validation_error=fake_news_error,
household_jacobian_seconds=hhJ_seconds,
ge_composition_seconds=ge_seconds,
steady_target_error=maximum(abs.(ge.steady_residual)),
linear_goods_error=maximum(abs.(irf.goods)),
distribution_mass=sum(h.D),
equity_steady_error=abs(m.equity_residual)
)
println("11.2 Jacobian, equilibrium, and Walras-law diagnostics")
println(repeat("-", 100))
@printf("%-42s %20s\n", "Jacobian construction method", string(checks.jacobian_method))
@printf("%-42s %19.3f s\n", "Household Jacobian time", checks.household_jacobian_seconds)
@printf("%-42s %19.3f s\n", "GE block-composition time", checks.ge_composition_seconds)
println(repeat("-", 100))
@printf("%-42s %14s %18s %10s\n", "diagnostic", "value", "reference tolerance", "status")
println(repeat("-", 100))
check_rows = [
("Fake-news vs brute-force Jacobian error", checks.fake_news_validation_error, 5e-4),
("Steady-state target residual", checks.steady_target_error, 1e-8),
("Linearized goods-market residual", checks.linear_goods_error, 1e-4),
("Stationary-distribution mass error", abs(checks.distribution_mass - 1), 1e-10),
("Steady-state equity-pricing error", checks.equity_steady_error, 1e-10),
]
for (label, value, tolerance) in check_rows
@printf("%-42s %14.6e %18.1e %10s\n",
label, value, tolerance, value < tolerance ? "PASS" : "CHECK")
end
println(repeat("-", 100))
println("The goods-market residual is a Walras-law check; it was not one of the five GE solver targets.")
12) MPC, Hand-to-Mouth Households, and Data Moments
12.1 Finite-transfer MPC experiment
We follow the calculation in the one-asset HANK notebook, but propagate both liquid and illiquid choices. The date-zero transfer is normalized to 3 percent of mean quarterly labor income,
$$ x=0.03\,\bar y^{\ell},\qquad \bar y^{\ell}=\sum_{e,b,a}D_{ss}(e,b,a)w_{ss}e n_{ss}(e,b,a). $$
The continuation marginal values are held fixed in the impact period. After the initial policy response, households follow the stationary two-asset policy. For horizon $H$ quarters,
$$ \operatorname{MPC}_{H}(e,b,a) =\frac{\mathcal C_H^x(e,b,a)-\mathcal C_H^0(e,b,a)}{x}, \qquad \overline{\operatorname{MPC}}_H =\sum_{e,b,a}D_{ss}(e,b,a)\operatorname{MPC}_H(e,b,a). $$
The cumulative-consumption recursion integrates the earnings transition and the interpolation lotteries for both $b'$ and $a'$.
micro = kmv_mpc_statistics(ss; transfer_share=0.03, horizons=(1, 2, 4))
println("Transfer normalization and cumulative MPCs")
println(repeat("-", 104))
@printf("Mean quarterly labor income (model units) %10.6f\n", micro.mean_labor_income)
@printf("One-time transfer (model units) %10.6f\n", micro.transfer)
@printf("Transfer / mean quarterly labor income %9.3f%%\n", 100micro.transfer_share)
@printf("Baseline policy replay error %10.3e\n", micro.replay_error)
println()
println(" horizon mean median p90 share MPC>=0.5 poor HtM wealthy HtM non-HtM")
for horizon in (1, 2, 4)
stat = micro.horizon_statistics[horizon]
@printf(" %3d qtrs %8.4f %8.4f %8.4f %8.3f%% %8.4f %8.4f %8.4f\n",
horizon, stat.mean, stat.median, stat.p90, 100stat.high_mpc_share,
stat.poor_htm_mean, stat.wealthy_htm_mean, stat.non_htm_mean)
end
12.2 KMV hand-to-mouth definitions
KMV's model targets use the exact endogenous kinks $b=0$ and $b<0$:
$$ \text{poor HtM}: b=0,\ a=0,\qquad \text{wealthy HtM}: b=0,\ a>0. $$
In survey data, KMV implement these concepts with a $\pm\$1{,}000$ window because wealth is measured with error. Since their $\$16{,}500$ borrowing limit is approximately one quarter of labor income, we additionally report the data-mapping sensitivity $|b|\leq\bar y^{\ell}/16.5$. Calibration below uses the exact model definitions; the dollar-band statistics are diagnostics only.
impact = micro.horizon_statistics[1]
println("KMV-style hand-to-mouth statistics")
println(repeat("-", 76))
@printf("Exact-model poor HtM share %9.3f%%\n", 100micro.poor_htm_share)
@printf("Exact-model wealthy HtM share %9.3f%%\n", 100micro.wealthy_htm_share)
@printf("Total HtM share %9.3f%%\n", 100micro.total_htm_share)
@printf("Wealthy households / all HtM %9.3f%%\n", 100micro.wealthy_share_of_htm)
@printf("Exact-model negative-liquid-wealth share %9.3f%%\n", 100micro.negative_liquid_share)
println()
@printf("Data-mapping liquid band +/- %9.6f\n", micro.liquid_band)
@printf("Band poor / wealthy HtM shares %8.3f%% / %8.3f%%\n",
100micro.band_poor_htm_share, 100micro.band_wealthy_htm_share)
@printf("Share below the lower edge of the band %9.3f%%\n",
100micro.band_negative_liquid_share)
@printf("HtM share of aggregate consumption %9.3f%%\n", 100micro.htm_consumption_share)
@printf("HtM share of aggregate net wealth %9.3f%%\n", 100micro.htm_wealth_share)
println()
@printf("Impact MPC: aggregate / poor / wealthy / non-HtM = %.4f / %.4f / %.4f / %.4f\n",
impact.mean, impact.poor_htm_mean, impact.wealthy_htm_mean, impact.non_htm_mean)
12.3 Current model and KMV model moments
The comparison below uses the same observable definitions as KMV whenever the current discrete-time, no-death model permits it. Each row reports only the current model value and the corresponding value in KMV's calibrated model.
Mean asset stocks are divided by annual output, $4Y$, because this notebook is quarterly. The first five rows correspond to KMV Table 5; the remaining rows report HtM consumption and wealth shares and the one-quarter MPC.
moment_comparison = kmv_data_moment_comparison(ss, micro)
println("Current discrete-time model versus KMV model")
println(repeat("-", 76))
@printf("%-41s %15s %15s\n", "moment", "current model", "KMV model")
println(repeat("-", 76))
for row in moment_comparison
@printf("%-41s %15.4f %15.4f\n",
row.moment, row.current_model, row.paper_model)
end
println(repeat("-", 76))
wealth_rows = moment_comparison[1:2]
micro_rows = moment_comparison[3:end]
wealth_labels = ["Illiquid / annual Y", "Liquid / annual Y"]
wealth_x = collect(eachindex(wealth_labels))
wealth_model = getproperty.(wealth_rows, :current_model)
wealth_paper = getproperty.(wealth_rows, :paper_model)
p_wealth_match = bar(wealth_x .- 0.16, wealth_model;
bar_width=0.30, label="current model", color=:steelblue,
title="Aggregate wealth moments", ylabel="ratio to annual output",
xticks=(wealth_x, wealth_labels), legend=:topright)
bar!(p_wealth_match, wealth_x .+ 0.16, wealth_paper;
bar_width=0.30, label="KMV model", color=:gray)
micro_labels = ["Poor HtM", "Wealthy HtM", "b below band", "HtM C share", "HtM W share", "Impact MPC"]
micro_x = collect(eachindex(micro_labels))
micro_model = getproperty.(micro_rows, :current_model)
micro_paper = getproperty.(micro_rows, :paper_model)
p_micro_match = bar(micro_x .- 0.16, micro_model;
bar_width=0.30, label="current model", color=:steelblue,
title="Micro moments", ylabel="share or MPC",
xticks=(micro_x, micro_labels), xrotation=18, legend=:topright)
bar!(p_micro_match, micro_x .+ 0.16, micro_paper;
bar_width=0.30, label="KMV model", color=:gray)
plot(p_wealth_match, p_micro_match; layout=(1, 2), size=(1500, 560), margin=6Plots.mm)
Interpretation. The main mismatches are the poor HtM share, the share of households with negative liquid wealth, the one-quarter MPC, and the HtM wealth share.
The current model produces a poor HtM share of 0.5 percent, far below KMV's 10 percent, and a one-quarter MPC of 10.1 percent, below the KMV model's 16 percent. By contrast, 55.3 percent of households hold negative liquid wealth, compared with 15 percent in KMV. The HtM wealth share is 17.9 percent, versus 2.5 percent in the KMV model and 4.4 percent in the data.
These discrepancies motivate the selected-parameter calibration in Section 13, where we examine how far the discrete-time, no-death model can move toward the KMV targets.
13) Calibration and External Validation
13.1 Calibrated parameters and target moments
We calibrate five household parameters while retaining the notebook's discrete-time, no-death structure:
| parameter | selected value | role in the calibration |
|---|---|---|
| $\beta$ | $0.98145$ | overall household saving, especially illiquid wealth |
| $\kappa_b$ | $0.0200$ | cost of unsecured borrowing and the mass with $b<0$ |
| $\chi_0$ | $0.0200$ | linear adjustment wedge and the mass in the inaction region |
| $\chi_1$ | $0.3400$ | scale of convex illiquid adjustment costs |
| $\chi_2$ | $1.4020$ | curvature of convex illiquid adjustment costs |
The five target moments are
$$ \frac{A}{4Y}=2.92,\qquad \frac{B}{4Y}=0.26,\qquad \Pr(b=0,a=0)=0.10,\qquad \Pr(b=0,a>0)=0.20,\qquad \Pr(b<0)=0.15. $$
These are data targets, not the moments mechanically delivered by the selected parameters. Model HtM moments use the exact $b=0$ atoms. The empirical measures use the $\pm\$1,000$ measurement window described in KMV's footnote 37. The cells below report only the selected parameter values and their moment fit.
calibration_targets = (
illiquid_to_annual_output=2.92,
liquid_to_annual_output=0.26,
poor_htm_share=0.10,
wealthy_htm_share=0.20,
negative_liquid_share=0.15,
)
selected_calibration = (
beta=0.98145,
borrowing_wedge=0.0200,
chi0=0.0200,
chi1=0.3400,
chi2=1.4020,
)
calibrated_params = KMVDiscrete.kmv_replace_parameters(
params;
beta=selected_calibration.beta,
borrowing_wedge=selected_calibration.borrowing_wedge,
chi0=selected_calibration.chi0,
chi1=selected_calibration.chi1,
chi2=selected_calibration.chi2,
);
ss_calibrated = calibrate_kmv_steady_state(
calibrated_params, macro_params;
Rb=1.005, Ra=1.01425, w=1.05, T=0.10,
backward_tol=6e-6, forward_tol=5e-9, damp=0.8,
)
calibrated_moments = kmv_calibration_moments(
ss_calibrated; annual_output=4ss_calibrated.macro_ss.Y)
micro_calibrated = kmv_mpc_statistics(
ss_calibrated; transfer_share=0.03, horizons=(1, 2, 4))
calibration_fit = [
(moment="Illiquid assets / annual output",
model=calibrated_moments.illiquid_to_annual_output,
target=calibration_targets.illiquid_to_annual_output),
(moment="Liquid assets / annual output",
model=calibrated_moments.liquid_to_annual_output,
target=calibration_targets.liquid_to_annual_output),
(moment="Poor HtM: Pr(b=0,a=0)",
model=calibrated_moments.poor_htm_share,
target=calibration_targets.poor_htm_share),
(moment="Wealthy HtM: Pr(b=0,a>0)",
model=calibrated_moments.wealthy_htm_share,
target=calibration_targets.wealthy_htm_share),
(moment="Negative-liquid-wealth share",
model=calibrated_moments.negative_liquid_share,
target=calibration_targets.negative_liquid_share),
]
println("Calibration fit")
println(repeat("-", 66))
@printf("%-38s %12s %12s\n", "target moment", "model", "data target")
println(repeat("-", 66))
for row in calibration_fit
@printf("%-38s %12.4f %12.4f\n", row.moment, row.model, row.target)
end
println(repeat("-", 66))
asset_labels = ["Illiquid / annual Y", "Liquid / annual Y"]
share_labels = ["Poor HtM", "Wealthy HtM", "Pr(b<0)"]
asset_x, share_x = 1:2, 1:3
p_calibration_assets = bar(
collect(asset_x) .- 0.16, getproperty.(calibration_fit[1:2], :model);
bar_width=0.30, color=:steelblue, label="calibrated model",
xticks=(asset_x, asset_labels), ylabel="ratio to annual output",
title="Asset-stock target fit",
)
bar!(p_calibration_assets, collect(asset_x) .+ 0.16,
getproperty.(calibration_fit[1:2], :target);
bar_width=0.30, color=:darkorange, label="target")
p_calibration_shares = bar(
collect(share_x) .- 0.16, getproperty.(calibration_fit[3:5], :model);
bar_width=0.30, color=:steelblue, label="calibrated model",
xticks=(share_x, share_labels), ylabel="population share",
title="Distributional target fit",
)
bar!(p_calibration_shares, collect(share_x) .+ 0.16,
getproperty.(calibration_fit[3:5], :target);
bar_width=0.30, color=:darkorange, label="target")
plot(p_calibration_assets, p_calibration_shares;
layout=(1, 2), size=(1500, 540), margin=6Plots.mm)
validation_rows = [
(moment="One-quarter MPC", model=micro_calibrated.horizon_statistics[1].mean,
data_low=0.15, data_high=0.25),
(moment="HtM consumption share", model=micro_calibrated.htm_consumption_share,
data_low=0.25, data_high=0.25),
(moment="HtM wealth share", model=micro_calibrated.htm_wealth_share,
data_low=0.044, data_high=0.044),
]
println("External validation: none of these moments was targeted")
println(repeat("-", 78))
@printf("%-30s %12s %20s\n", "moment", "model", "data target/range")
for row in validation_rows
target = row.data_low == row.data_high ?
@sprintf("%.4f", row.data_low) : @sprintf("[%.4f, %.4f]", row.data_low, row.data_high)
@printf("%-30s %12.4f %20s\n", row.moment, row.model, target)
end
println(repeat("-", 78))
Interpretation. After calibration, the poor HtM share remains much lower than the 10 percent target, while the share of households with negative liquid wealth remains much higher than the 15 percent target.
For external validation, the one-quarter MPC is 10.6 percent, below the empirical range of 15--25 percent. The HtM consumption share, 22.8 percent, is fairly close to the 25 percent target, but the HtM wealth share, 28.4 percent, remains much higher than the empirical 4.4 percent.
Part of these remaining gaps reflects model structure. Unlike KMV, this notebook excludes mortality and newborn replacement, a mechanism that continuously introduces zero-wealth households and can be important for the poor HtM mass. The model also uses a simple discrete AR(1) earnings process, whereas KMV use a richer empirically estimated continuous-time earnings process with persistent and transitory movements and infrequent jumps.
14) Consumption-Channel Decomposition
Consider the same expansionary monetary-policy innovation used above,
$$ d\eta_t=-0.0025\times0.6^t. $$
This section uses the calibrated steady state from Section 13. General equilibrium first determines the five household-input paths $(dR^b,dR^a,dw,dT,d\tau)$. The two-asset household Jacobians then imply
$$ dC= \underbrace{J_{C,R^b}dR^b}_{\text{direct effect}} +\underbrace{J_{C,R^a}dR^a+J_{C,w}dw+J_{C,T}dT +J_{C,\tau}d\tau}_{\text{indirect general-equilibrium effects}}. $$
Following Kaplan, Moll, and Violante (2018), only the liquid-return term is classified as direct: it varies $R^b$ while holding disposable labor income and all other household inputs fixed. The illiquid-return/equity-value, wage, transfer, and tax paths are equilibrium outcomes and are therefore indirect. In this discrete implementation $q$ is not a separate household input; its equilibrium movement is shown with the price paths, while the household consumption contribution is carried by $dR^a$. The baseline fiscal rule keeps $\tau$ fixed, so its channel is zero. At first order all components must add exactly to the total consumption IRF.
As in KMV Table 7, the summary reports both the impact response and the average response over the first four quarters.
T_channels = T_GE
hhJ_channels = kmv_household_fake_news_jacobian(
ss_calibrated, T_channels; h=2e-5
)
ge_channels = solve_kmv_ge_jacobian(
ss_calibrated, T_channels; household_J=hhJ_channels, hU=2e-5, hE=2e-5
)
channel_shock = zeros(2T_channels)
channel_shock[T_channels+1:2T_channels] .= -0.0025 .* 0.6 .^ (0:T_channels-1)
consumption_channels = kmv_consumption_channel_decomposition(
ss_calibrated, ge_channels, channel_shock
)
consumption_channels.closure_error < 1e-10 ||
error("Consumption channels do not add up")
channel_summary = print_kmv_consumption_channel_summary(
consumption_channels, ss_calibrated;
label="calibrated-model monetary easing", first_year=4
);
plot_kmv_consumption_channels(
consumption_channels, ss_calibrated;
horizon=40,
title="Direct and indirect effects of a monetary easing in two-asset HANK",
)
14.1 Monetary easing: interpretation
For the calibrated discrete-time model, the first-year average consumption response is $0.1765\%$ of steady-state consumption. The liquid-return direct effect contributes $22.64\%$ of that response, while all indirect general-equilibrium channels contribute $77.36\%$. Within the indirect component, wages account for $41.71\%$, transfers for $37.90\%$, and the illiquid-return/equity-value channel for $-2.25\%$; the fixed tax rule makes the tax contribution zero.
The economic mechanism is therefore not that households consume mainly because the liquid real rate falls. Monetary easing raises labor demand and labor income, and the government budget rule passes lower debt-service costs through to transfers. Heterogeneous MPCs turn those income changes into a large aggregate consumption response. The small negative $R^a/q$ term is the portfolio-rebalancing force: a relatively more attractive illiquid account redirects some resources away from current consumption.
These shares are a diagnostic of this notebook's discrete-time, no-death calibration rather than a numerical replication of KMV Table 7. The close qualitative result is the object of interest: indirect channels dominate, and wages and transfers provide the largest contributions.
14.2 Positive TFP shock
We now apply the same decomposition to a one-standard-deviation positive
productivity shock, using the shock calibration in HANK.ipynb for direct
comparability:
$$ d\log Z_t=\sigma_Z\rho_Z^t,\qquad \rho_Z=0.9,\qquad \sigma_Z=0.07\sqrt{1-0.9^2}. $$
TFP does not enter the household budget constraint as a separate input. It
first changes production, labor demand, marginal cost, asset returns, and the
government budget. General equilibrium therefore generates the five household
input paths $(dR^b,dR^a,dw,dT,d\tau)$, which are then passed through the
same household Jacobians as in Section 14.1. In the table below, direct means
the contribution of the liquid-return input to the household problem; it does
not mean that TFP itself enters household utility or the budget directly.
rho_tfp = 0.9
sigma_tfp = 0.07 * sqrt(1 - rho_tfp^2)
tfp_path = sigma_tfp .* rho_tfp .^ (0:T_channels-1)
tfp_channel_shock = vcat(tfp_path, zeros(T_channels))
tfp_consumption_channels = kmv_consumption_channel_decomposition(
ss_calibrated, ge_channels, tfp_channel_shock
)
tfp_consumption_channels.closure_error < 1e-10 ||
error("TFP consumption channels do not add up")
tfp_channel_summary = print_kmv_consumption_channel_summary(
tfp_consumption_channels, ss_calibrated;
label="positive one-sigma TFP shock", first_year=4
);
plot_kmv_consumption_channels(
tfp_consumption_channels, ss_calibrated;
horizon=40,
title="Direct and indirect effects of a positive TFP shock in two-asset HANK",
)
Interpretation. The positive TFP shock raises first-year average consumption by $1.59279\%$ of steady-state consumption. The liquid-return channel contributes $12.50\%$ of the response. All indirect channels together contribute $87.50\%$: wages account for $51.75\%$, transfers for $23.85\%$, and the illiquid-return/equity-value block for $11.90\%$. The tax channel is zero because the fiscal rule keeps $\tau$ fixed. The maximum adding-up error is $8.7\times10^{-18}$.
The wage channel is the largest because higher productivity expands output and changes firms' labor demand, producing a sizable equilibrium labor-income path. Households with high MPCs translate that additional income into consumption. The transfer channel is also positive because the expansion raises the labor-tax base in the government budget rule. The two-asset model adds a further positive portfolio contribution through the induced $R^a/q$ path. Finally, the positive liquid-return contribution is the household response to the Fisher/Taylor-rule path generated by TFP; it is not a primitive direct effect of productivity on household utility or resources. Relative to Section 14.1, indirect channels are even more dominant for the productivity shock.