Your own model¶
hetlearn works with any linear IRIS model that has forward-looking terms. You do not change
the model to add learners: the toolbox reads your model file, finds every lead, and writes an
extended version in which the expectations of the variables you choose are a mix of RE agents
and learners. This page takes you from a model file to a first run. The worked example is the
three-equation New Keynesian model of Özden (2025), package models/ijcb3/ in the repository.
What you need¶
| File | What it is |
|---|---|
models/<name>/<name>.model |
The IRIS model file, linear, with leads written as x{+1} |
models/<name>/<name>_params.m |
A function returning a struct with every parameter and every shock standard deviation |
models/<name>/data/<name>_database.csv |
The observables as an IRIS databank (CSV from databank.toCSV, or a .mat) |
models/<name>/model_spec.m |
The package contract: a function returning spec |
scenarios/<name>/.../*.m |
Your scenario files |
Step 1: the model file¶
A plain IRIS model file. The toolbox only needs leads of transition variables; every
x{+1} becomes an expectation that can be heterogeneous.
!transition_variables
"Output gap" y
"Inflation, % q/q" pinf
"Nominal policy rate, % per quarter" r
"Demand shock process" u_y
"Cost-push shock process" u_pinf
!transition_shocks
"Demand innovation" eps_y
"Cost-push innovation" eps_pinf
"Monetary policy shock, i.i.d." eps_r
!parameters
beta, tau, kappa, iota_y, iota_p, phi_pi, phi_y, phi_dy, rho_r, rho_y, rho_pi, pi_bar, r_bar_T
!transition_equations
"IS curve, hybrid (IJCB eq. 1)"
y = (1 - iota_y)*y{+1} + iota_y*y{-1} - (1/tau)*(r - pinf{+1}) + u_y;
"Phillips curve, hybrid (IJCB eq. 1)"
pinf = beta*((1 - iota_p)*pinf{+1} + iota_p*pinf{-1}) + kappa*y + u_pinf;
"Monetary policy rule, Taylor rule with smoothing (IJCB eq. 1)"
r = rho_r*r{-1} + (1 - rho_r)*(phi_pi*pinf + phi_y*y) + phi_dy*(y - y{-1}) + eps_r;
u_y = rho_y*u_y{-1} + eps_y;
u_pinf = rho_pi*u_pinf{-1} + eps_pinf;
!measurement_variables
yobs, pinfobs, robs
!measurement_equations
yobs = y;
pinfobs = pi_bar + pinf;
robs = r_bar_T + r;
Notes:
- Lead notation. IRIS 20230622 reads
x{+1}andx(+1). The toolbox also acceptsx{1},x[+1],x{t+1}and//or/* */comments, and converts them for IRIS (it then keeps a normalised copy,generated/<name>_norm.model). - Leads of two or more periods work:
x{+2}becomes a chain through the one-step expectation. - Equation labels (the quoted text before an equation) are optional, but the policy rule needs one if you want Ramsey policy, and equation-specific transforms refer to labels.
- Your own
!ifswitches are allowed; set their values inspec.assign. - Linear models only.
spec.linear = true.
Step 2: the parameters¶
A function in the package folder that returns a struct with every parameter, including the
standard deviation of every shock as std_<shock> (and of measurement shocks, if any):
function P = ijcb3_params()
% Ozden (2025), Table 1, heterogeneous-expectations posterior means
P = struct();
P.beta = 0.99; % fixed
P.tau = 1.13;
P.kappa = 0.0032;
P.iota_y = 0.06;
P.iota_p = 0.15;
P.phi_pi = 1.53;
P.phi_y = 0.42;
P.phi_dy = 0.12;
P.rho_r = 0.92;
P.rho_y = 0.95;
P.rho_pi = 0.04;
P.pi_bar = 0.58; % trends in the measurement equations, % per quarter
P.r_bar_T = 0.26;
P.std_eps_y = 0.32; % one std_<shock> per shock
P.std_eps_pinf = 0.38;
P.std_eps_r = 0.24;
end
You can keep several parameter files in the package and switch between them with the option
model_params. The project uses published parameter values; the toolbox does not estimate
them.
Step 3: the database¶
An IRIS databank with one quarterly series per observable, saved as CSV:
d = struct();
d.yobs = Series(qq(1957, 1), yobs_values); % column vectors of data
d.pinfobs = Series(qq(1957, 1), pinfobs_values);
d.robs = Series(qq(1957, 1), robs_values); % NaN where the rate is not observed
databank.toCSV(d, fullfile('models', 'ijcb3', 'data', 'ijcb3_database.csv'));
Missing values (NaN) are fine: the Kalman filter skips them. The ijcb3 package treats the federal funds rate as missing at the effective lower bound, so the filter infers a shadow rate.
Step 4: the package contract¶
models/<name>/model_spec.m, a function with no inputs:
function spec = model_spec()
spec = struct();
spec.name = 'ijcb3'; % used in generated file names
spec.folder = fileparts(mfilename('fullpath')); % always this line
spec.model_file = 'ijcb3.model'; % relative to the folder
spec.params = 'ijcb3_params'; % parameter function
spec.sstate = ''; % steady-state function (nonlinear models); '' here
spec.linear = true;
spec.assign = struct(); % values of the model file's own !if switches
spec.database = 'data/ijcb3_database.csv';
spec.filter_range = [qq(1960, 1), qq(2026, 2)]; % RE filter and smoother range
spec.observables = {'yobs', 'pinfobs', 'robs'};
% Optional: reporting labels, units and the scale to report units (4 annualises a quarterly rate)
report = struct();
report.labels = struct('yobs', 'Output gap', 'pinfobs', 'Inflation (CPI)', 'robs', 'Federal funds rate');
report.units = struct('yobs', '% of trend output', 'pinfobs', '% q/q annualised', 'robs', '% annualised');
report.scale = struct('yobs', 1, 'pinfobs', 4, 'robs', 4);
spec.report = report;
% Optional: needed only for Ramsey policy and the policy loss
% spec.policy = struct('instrument', 'r', ...
% 'rule_label', 'Monetary policy rule, Taylor rule with smoothing (IJCB eq. 1)', ...
% 'targets', struct('pinf', '4*pinf', 'ygap', 'y', 'r', '4*r', 'dr', '4*(r - r{-1})'), ...
% 'discount', 'beta');
end
Required fields: name, model_file, params, linear. The others default to empty.
Step 5: generate and check¶
setup_paths
spec = hetlearn.load_spec('ijcb3');
cfg = struct('model', 'ijcb3');
cfg.expectations.variables = {'pinf'};
opts = hetlearn.load_options(cfg, struct()); % struct() = no project master options
[m, info] = hetlearn.load_model(spec, opts); % writes generated/ijcb3_ext_ar1.model, runs the collapse test
info.meta.forward_vars % the variables that can learn: y, pinf
H = hetlearn.re_filter(spec, opts); % RE filter and smoother, cached
H.fit_error % how well the smoothed states rebuild the data
hetlearn.load_model checks every generated file with a collapse test: with every RE share
at 1 the extended model must reproduce the impulse responses of your original model to 1e-10.
hetlearn.re_filter stops if the smoothed states and shocks do not rebuild the data to 1e-8
(when the model has at least as many shocks as observables).
The second argument of hetlearn.load_options replaces config/master_options.m. Use it while
you set up a new model if the project's master options hold values for another model (the
SW07 baseline sets model_params = 'sw07_params_original', a file of the SW07 package).
Step 6: scenarios¶
Start the new model's RE parent from an empty struct, not from an SW07 scenario (sw07_re
loads SW07 and checks the SW07 inflation peak):
function cfg = i1_re()
cfg = struct();
cfg.id = 'i1_re';
cfg.title = 'IJCB3: forecast from the 2022Q2 CPI inflation peak, rational expectations';
cfg.model = 'ijcb3';
spec = hetlearn.load_spec(cfg.model);
db = hetlearn.load_database(spec);
cfg.forecast.origin = hetlearn.find_peak(db, 'pinfobs', qq(2020, 1), qq(2024, 4));
cfg.forecast.horizon = 16;
cfg.expectations.mode = 'rational';
cfg.expectations.variables = {'pinf'};
cfg.model_params = ''; % this package's own parameters, whatever the master says
end
function cfg = i1_learn()
cfg = i1_re();
cfg.id = 'i1_learn';
cfg.expectations.mode = 'switching';
cfg.gain.value = 0.0585; % Ozden (2025), Table 1
cfg.switching.memory = 0.62;
cfg.switching.intensity = 0.51;
cfg.compare_with = {'i1_re'};
end
Save them under scenarios/ijcb3/app1_peak/ (run setup_paths again after creating a new
folder), then:
Values set in config/master_options.m reach these scenarios too. Pin anything the master
sets for another model, as cfg.model_params = '' does here.
What the toolbox writes into your model¶
For each learning variable x the extended model gets an expectation variable x_exp1 in
place of x{+1} and the block
with the RE agents' and the learners' forecasts behind !if switches that the toolbox sets
from the options:
| Switch | Set from | Effect |
|---|---|---|
br_<x> |
expectations.variables |
true: x's expectation is the RE/learner mix; false: pure RE |
re_anchored |
expectations.re_variant |
anchored RE agents instead of x{+1} |
ramsey |
policy.mode |
the loss function in place of the rule equation |
br_info_current |
beliefs.info |
learners see period-t data |
expectations.re_info = 'lagged' uses a second generated file,
<name>_ext_<tag>_relag.model, in which the RE expectations of the learning set use data up to
t-1. Generated files and RE filter caches live in models/<name>/generated/; their names carry
a hash of the model text, the PLM spec and the parameters, so a stale file is never used.
Delete the folder to force a rebuild.
Limits¶
- Linear models only;
spec.sstateexists for nonlinear models but no package uses it. - Anchored RE agents need plain state names (
hetlearn:load_model:anchoredotherwise). expectations.re_info = 'lagged'needs learning variables with a maximum lead of 1.- The inversion history needs at least as many shocks as observables.
- Ramsey policy needs
spec.policy.
Other templates in the repository¶
| Package | Model | Use as a template for |
|---|---|---|
models/ijcb3/ |
three-equation NK model, Özden (2025) | a small model with missing data (ELB) |
models/as07/ |
An and Schorfheide (2007) | measurement errors (std_me_* in the parameter file) and annualised observables (report scale 1) |
models/sw07/ |
Smets and Wouters (2007) | a medium-scale model, two parameter sets, a policy section |
tests/toy_nk/ |
toy hybrid NK model | mixed lead notations, a two-period lead, a policy section with quarterly targets |
Each package has a README.md, a data/README.md with build_database.m, and a
validation/ folder that checks the package against Dynare or the authors' code.