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

@heterogeneity_axis(a, 10, policy = ap)

Endogenous (policy) axis. The grid (here a, 10 points) is an individual state whose position is chosen by the solved policy variable ap. Use this for assets, capital, wealth – anything an agent decides. (Selected by policy =, not evolution =.)

@heterogeneity_axis(e, 2) (or ..., evolution = transition)

Exogenous Markov axis. evolution = transition is the default, so the option is usually omitted. The position evolves stochastically, e.g. an idiosyncratic income/skill process.

@heterogeneity_axis(j, 5, evolution = fixed)

Fixed-type axis. A permanent characteristic that never changes (a household type); mass is conserved within each type.

@heterogeneity_axis(s, 5, evolution = deterministic, boundary = cyclic)

Deterministic axis. Each agent moves one grid point forward per period; the boundary option (cyclic is the default) sets what happens at the top of the grid. Use this for a lifecycle/age dimension. Creates no transition probabilities. See Deterministic axes.

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 (for i not equal to j); each diagonal probability is the residual 1 - 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 periods 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,

\[\begin{split}T_{\text{cyclic}} = \begin{bmatrix} 0&1&0&0\\ 0&0&1&0\\ 0&0&0&1\\ 1&0&0&0 \end{bmatrix}, \quad T_{\text{absorbing}} = \begin{bmatrix} 0&1&0&0\\ 0&0&1&0\\ 0&0&0&1\\ 0&0&0&1 \end{bmatrix}, \quad T_{\text{reflecting}} = \begin{bmatrix} 0&1&0&0\\ 0&0&1&0\\ 0&0&0&1\\ 0&0&1&0 \end{bmatrix}.\end{split}\]

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:

  • cyclic sends 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.

  • absorbing holds 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.

  • reflecting sends 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 variablec{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.