1.10. Heterogeneous agents (HANK)
A heterogeneous-agent model (HANK, and more generally any
incomplete-markets / Aiyagari-type economy) is authored in the
ordinary .rs grammar with three additional constructs.
RISE reads the annotated file and expands it into a plain
Markov-switching DSGE; from that point the object behaves like any
other model – solve, filter, estimate, forecast,
simulate, irf and the decompositions all apply, and the
heterogeneity composes with regime switching.
A model file that uses none of these constructs is an ordinary representative-agent model and is parsed unchanged.
The three constructs are:
@heterogeneity_axis(...)– declare a dimension of the cross-section (a grid).@endogenous(individual)– declare the variables solved at the individual (per-grid-point) level.@Agg(...)– aggregate an individual expression across the cross-section.
This chapter documents what each construct means and what it does. It is a parser-level interface: the model you write is the model that is solved. The solution itself is handled by RISE’s standard perturbation machinery, with the cross-section treated as an extension of Reiter’s method; that part is internal and not the subject of this chapter.
1.10.1. Heterogeneity axes
Each dimension of the cross-section is declared with one line:
@heterogeneity_axis(name, size, <option>)
name is the axis label, size is the number of grid points
(a literal, or a macro/rise_flags name). The third field
selects the kind of axis, in one of two ways: policy = <var>
makes it an endogenous axis, and evolution = <kind> selects
any other kind, where <kind> is one of transition (the
default), fixed or deterministic:
Declaration |
Meaning |
|---|---|
|
Endogenous (policy) axis. The grid (here |
|
Exogenous Markov axis. |
|
Fixed-type axis. A permanent characteristic that never changes (a household type); mass is conserved within each type. |
|
Deterministic axis. Each agent moves one grid point
forward per period; the |
For every axis, RISE automatically creates the grid-value
parameters name_1, name_2, ..., name_size. In addition:
an exogenous Markov axis creates the off-diagonal transition probabilities
name_htp_i_j(forinot equal toj); each diagonal probability is the residual1 - sum of its row's off-diagonals;a fixed-type axis creates the type masses
name_mass_i;a deterministic axis creates no transition parameters – its transition matrix is known (see Deterministic axes).
You only declare the axes; you supply their numeric content the same way as any other parameter – see Setting grid and transition values below.
Inside an equation, the axis name used bare (a, e) is
the coordinate of the current grid point – a fixed label, equal
to the grid value at that point. A coordinate is not a variable: it
carries no {t}, {t+1} or {t-1} and has no law of
motion.
1.10.2. Deterministic axes
A deterministic axis moves each agent one grid point forward per
period – s to s+1 – with no randomness. Its transition
matrix is therefore known, so RISE creates no name_htp_i_j
parameters (only the grid values name_i). This is the usual
representation of a lifecycle / age dimension.
The only freedom is what happens at the top of the grid, set by
boundary (default cyclic). For a four-point axis the
transition matrix \(T\) (row \(i\) = current point, column
\(j\) = next point; \(T_{ij}=1\) when an agent at \(i\)
moves to \(j\)) is, under each rule,
Every row has a single 1 – each agent has exactly one
destination – so mass is conserved under all three rules. They
differ only in the last row:
cyclicsends the top point back to the first (the matrix is a permutation): “die at the top, reborn at the bottom”. It is the default and the only rule that is non-degenerate on its own.absorbingholds agents at the top point (a terminal / catch-all bin); mass drifts up and the lower bins drain unless they are refilled by another (stochastic) axis.reflectingsends the top point back one step.
Because the destination is known, two things simplify relative to a
Markov axis: the distribution law of motion is sparser – each
cell receives from a single predecessor along the deterministic axis
– and an individual lead c{t+1} across a deterministic axis is
simply the value at the next point (there is no expectation to
take):
@heterogeneity_axis(a, 10, policy = ap) % assets
@heterogeneity_axis(s, 6, evolution = deterministic) % age, cyclic (default)
@heterogeneity_axis(s, 6, evolution = deterministic, boundary = absorbing)
1.10.3. Individual variables
The variables solved at each grid point are declared with the
individual qualifier:
@endogenous(individual)
c "consumption", ap "savings policy", mu "borrowing multiplier"
You declare each individual variable once. RISE replicates it
across the whole cross-section – one copy per grid cell. Optional
quoted descriptions are carried through to the per-cell variables,
as in the ordinary @endogenous block.
Aggregate variables, shocks and parameters are declared exactly as
usual with @endogenous, @exogenous and @parameters.
1.10.4. The individual block
You write each individual equilibrium condition once, in the
@model block, including its first-order conditions – RISE does
not take FOCs or reformulate anything. The single equation is
copied to every grid cell.
Two rules govern how symbols in an individual equation are read:
Individual variables carry time indices (
c{t},ap{t},c{t+1}); the bare axis coordinates do not (a,e).A lead on an individual variable –
c{t+1}– is read as the idiosyncratic conditional expectation: the expectation over the agent’s own movement across the cross-section. A lead on an aggregate variable –r{t+1}– keeps the ordinary \(E_t\). You simply write the lead; RISE forms the appropriate expectation.
For example, a consumption Euler equation:
c{t}^(-sigma) = beta*(1+r{t+1})*c{t+1}^(-sigma) + mu{t};
Here c{t+1} is individual (idiosyncratic expectation over the
agent’s next position), r{t+1} is aggregate (ordinary
\(E_t\)), and mu is the individual multiplier on the
borrowing limit.
A budget constraint using the bare coordinates:
c{t} = (1+r{t})*a + w{t}*e - ap{t};
a is the agent’s current asset position and e the current
income state – both fixed grid labels at this cell.
Occasionally-binding individual constraints (a borrowing limit) are written as your own kinky equation, exactly as the zero-lower-bound is written in the representative-agent case (see Occasionally-binding constraints):
max( -mu{t}, amin - ap{t} ) = 0;
The complementarity form is passed through untouched.
1.10.5. Aggregation: @Agg
@Agg(expr) is the cross-sectional aggregate of an
individual expression – its integral over the population
distribution. It is the only place the distribution enters the
model you write:
A{t} = @Agg(ap{t}); % aggregate savings = integral of ap
C{t} = @Agg(c{t}); % aggregate consumption
NE{t} = @Agg(e*n{t}); % effective labour supply
K{t} = @Agg(a); % aggregate assets = integral of the coordinate a
The argument is any individual expression – an individual variable,
a bare coordinate, or a product of them. @Agg is typically used
to form the aggregates that enter market-clearing conditions
(A{t} = B, K{t} = ...) and the aggregate block.
1.10.6. The aggregate block
Everything else is the ordinary representative-agent (“RANK”)
shell, written exactly as in a standard .rs file: firm
conditions, the monetary and fiscal rules, the New Keynesian
Phillips curve, market clearing, and the exogenous driving
processes. These equations reference the aggregates produced by
@Agg and the usual aggregate variables, with no special
syntax:
r{t} = alpha*exp(z{t})*(K{t}/L)^(alpha-1) - delta;
w{t} = (1-alpha)*exp(z{t})*(K{t}/L)^alpha;
A{t} = B; % bond-market clearing
z{t} = rho_z*z{t-1} + eps_z{t};
1.10.7. Setting grid and transition values
The grid values name_i, the Markov transitions name_htp_i_j
and the type masses name_mass_i are ordinary parameters. Set
them like any other parameter – in @parameterization, through
the macro rise_flags, or from the driver script with
set(m, 'parameters', ...). A typical driver computes a
discretised income process (for example a Rouwenhorst grid) for the
Markov axis and a chosen asset grid for the policy axis, then
assigns the resulting name_i and name_htp_i_j values:
m = dsge_model('my_hank.rs');
m = set(m, 'parameters', struct( ...
'a_1', 0, 'a_2', 0.26, ... % asset grid
'e_1', 0.54, 'e_2', 1.46, ... % income grid
'e_htp_1_2', 0.017, 'e_htp_2_1', 0.017)); % income transitions
The steady state is then solved (or imposed) as for any model, and
the object is ready for solve, irf, filter and the
rest.
1.10.8. Composing with regime switching
Heterogeneity and Markov switching are orthogonal. A switching
parameter block – @parameters(chain, N) name with its implicit
chain_tp_i_j transitions (see Model file language) –
is carried through the expansion unchanged, so the parameters of a
HANK model can switch across regimes exactly as in a
representative-agent model.
1.10.9. Worked example
A one-asset HANK (the replication target is the canonical sequence-jacobian one-asset example): an endogenous bond axis, an exogenous Rouwenhorst skill axis, individual consumption / savings / hours / borrowing multiplier, and a New Keynesian aggregate shell:
% ---- cross-section axes ----
@heterogeneity_axis(a, 10, policy = ap) % endogenous bond axis
@heterogeneity_axis(e, 2) % exogenous skill axis -> e_htp_i_j
% ---- individual variables ----
@endogenous(individual)
c "consumption", ap "bond policy", n "hours", lam "borrowing multiplier"
% ---- aggregate variables, shocks, parameters ----
@endogenous Y w pai r rstar Z Div Tax A NE C L
@exogenous eps_r eps_z
@parameters beta eis frisch vphi mkp kappa phi B rho_r rho_z amin
@model
% individual block (written ONCE)
"Euler" c{t}^(-1/eis) = beta*(1+r{t+1})*c{t+1}^(-1/eis) + lam{t};
"Labor" vphi*n{t}^(1/frisch) = w{t}*e*c{t}^(-1/eis);
"Budget" c{t} + ap{t} = (1+r{t})*a + w{t}*e*n{t} + (Div{t}-Tax{t})*e;
max( -lam{t}, amin - ap{t} ) = 0;
% aggregation (the only place the distribution appears)
A{t} = @Agg(ap{t});
C{t} = @Agg(c{t});
NE{t} = @Agg(e*n{t});
% aggregate New Keynesian block (ordinary RANK equations)
L{t} = Y{t}/Z{t};
r{t} = (1 + rstar{t-1} + phi*pai{t-1})/(1 + pai{t}) - 1;
Tax{t} = r{t}*B;
A{t} = B; % bond-market clearing
NE{t} = L{t}; % labour-market clearing
rstar{t} = rho_r*rstar{t-1} + (1-rho_r)*rstar{stst} + eps_r{t};
Z{t} = rho_z*Z{t-1} + (1-rho_z)*1 + eps_z{t};
@parameterization
eis = 0.5; frisch = 0.5; mkp = 1.2; kappa = 0.1; phi = 1.5;
B = 5.6; amin = 0.0; rho_r = 0.0; rho_z = 0.0;
% grid + transition values (set here or overwritten by the driver)
a_1 = 0; a_2 = 0.26; % ... a_10 = 150;
e_1 = 0.537870; e_2 = 1.462130;
e_htp_1_2 = 0.017; e_htp_2_1 = 0.017;
The individual block is written once; c{t+1} is the
idiosyncratic expectation; a and e are the bin coordinates;
@Agg forms the aggregates that enter market clearing. Once
parsed, the object is an ordinary RISE model.