1.7. Optimal (optimized) simple rules

An optimal simple rule (OSR) keeps the model as written – including a simple, parametric feedback rule such as a Taylor rule – and chooses the rule coefficients that minimize a policy loss function. Unlike fully optimal policy (see Optimal policy), the model still has as many equations as endogenous variables: no equation is dropped, only some of its parameters are picked optimally.

1.7.1. The loss function

The loss is supplied to optimal_simple_rule at solve time, as a string in terms of the model’s variables and parameters (it is the planner’s objective, i.e. RISE maximises it – write a welfare measure, or minus a quadratic loss). A discount factor can be attached; it does not affect the ranking of unconditional losses, so it is often left at the default 0.99:

% minus a weighted quadratic loss in inflation, the output
% gap and the change in the policy rate
Loss = '-.5*(1*PAI^2 + .3*Y^2 + 0.9*DI^2)';

% with an explicit discount factor:
Loss = {0.99, '-.5*(1*PAI^2 + .3*Y^2 + 0.9*DI^2)'};

Important

The OSR objective is always supplied to optimal_simple_rule at solve time. It cannot be declared inside the model file: an @optimization_problem block in the .rs file is reserved strictly for optimal policy (the planner’s first-order conditions replace policy equations), which is a different solve concept from OSR (the policy equations are kept and only their coefficients are chosen optimally). See Optimal policy for the model-file block; here the loss is just a string argument.

The rule coefficients to optimize are listed the way estimated parameters are – through a priors structure giving each one a support (start value, lower bound, upper bound):

priors = struct();
priors.gam_lag = {0.60, 0.00,  1};     % {start, lb, ub}
priors.gam_y   = {0.50, 0.00, 10};

1.7.2. (Approximate) analytical loss

If the third argument is empty, optimal_simple_rule minimises the theoretical (unconditional) loss – computed from the model’s (perturbation-implied) second moments, with no simulation:

m    = dsge_model('Canonical_osr');
m    = set(m, parameters = p);

mest = optimal_simple_rule(m, Loss, [], estim_priors = priors);

print_estimation_results(mest)

1.7.3. Simulated loss

Passing a shocks database (or a struct / function handle producing one) as the third argument makes optimal_simple_rule minimize the conditional (simulated) loss along those shock paths instead – useful for nonlinear models or when you care about a specific scenario:

mest = optimal_simple_rule(m, Loss, shocks_db, estim_priors = priors);

You can evaluate the loss of any parameterization directly with calculate_loss(m, Loss) (unconditional) or calculate_loss(m, Loss, shocks_db) (conditional) – handy for comparing a hand-chosen rule with the optimized one.

1.7.4. OSR and indirect inference

Because OSR is solved exactly like an estimation problem – an objective minimized over a set of parameters with bounds – it composes with RISE’s estimation machinery: the same optimizers (see Estimation) are available, and OSR can be combined with indirect inference (choosing rule coefficients to match empirical moments or impulse responses) by supplying the corresponding objective.

1.7.5. OSR with a user-defined objective

optimal_simple_rule takes the loss as an expression – the string (or {discount, string} cell) described above; it does not accept a function handle. If the objective you want cannot be written that way – for example matching empirical moments or impulse responses – use indirect inference directly, treating the rule coefficients as the estimated parameters. There the objective is a function handle, with the signature:

[critmin, retcode] = myobjective(m)

where m is the solved model object, critmin is the scalar criterion to be minimized, and retcode is 0 when the evaluation succeeded.

1.7.6. Policy frontiers

To trace a policy frontier – how the model’s volatilities trade off as a preference parameter (a weight in the loss, the persistence of the rule, …) is varied – use frontier:

% vary the weight on the output gap over a 50-point grid
% between 0 and 2
f = frontier(mest, 'lamb_y', [0, 2]);

% or pass an explicit grid
f = frontier(mest, 'lamb_y', linspace(0, 2, 21));

% use simulated rather than theoretical moments
f = frontier(mest, 'lamb_y', [0, 2], true);

f is a structure of the standard deviations of every model variable at each grid point (plus a stats__ sub-structure with the grid, the number of simulation periods, and a per-point return code); plot the columns of interest against each other to get the frontier.