3. Structural VAR Modeling

An svar_model object represents a (possibly Markov-switching) structural vector autoregression – the structural form is parameterized and estimated directly, so there is no separate “estimate a reduced form, then rotate it” step. Almost everything else (data handling, estimation options, forecasting, decompositions, fan charts, bootstrap, Bayesian estimation, adding regime switching) works exactly as for reduced-form VARs; see Reduced-form VAR Modeling for the full worked example. This page covers what is specific to the structural case.

3.1. The model

\[A_{0}(r_{t}) y_{t} = C(r_{t}) x_{t} + A_{1}(r_{t}) y_{t-1} + \cdots + A_{p}(r_{t}) y_{t-p} + \varepsilon_{t}\]

with \(r_{t} = 1, 2, \dots, h\) and transition probabilities \(p_{r_{t}, r_{t+1}}(I_{t})\), and structural shocks \(\varepsilon_{t} \sim N(0, I)\).

  • \(A_{0}(r_{t})\) is the contemporaneous-impact matrix. It is normalized to have a unit diagonal (a0_i_i = 1), which fixes the scale of each structural shock.

  • \(A_{1}, \dots, A_{p}\) are the lag-coefficient matrices, and \(C\) the coefficients on the deterministic / exogenous block \(x_{t}\) (constant, declared exogenous regressors, and lags of \(y\)).

The estimated parameters are named by analogy with the matrices:

  • a0(row, col) – contemporaneous coefficients (off-diagonal entries of \(A_{0}\); the diagonal is fixed at 1).

  • a1(row, col), …, ap(row, col) – lag coefficients (a(row, col), with the lag index omitted, refers to all lags).

  • c(row, col) – coefficients on the exogenous / constant block.

row and col are integers or endogenous-variable names, e.g. a0(R, PAI) or a1(2, 3).

3.2. Creating an SVAR

endog = {'PAIOIL','GROWTH','PAI','R','EXRATE'};
exog  = {};
nlags = 4;
const = true;

mdl = svar_model(endog, ...
    lag_length         = nlags, ...
    constant_term      = const, ...
    deterministic_vars = exog);

(The factory mirrors rfvar_model’s shape; the legacy constructor signature was svar(varlist, exog, nlags, constant, markov_chains) positionally.)

3.3. Identification

A structural VAR with \(n\) endogenous variables has \(n(n - 1)/2\) more free parameters in \(A_{0}\) than the data can pin down, so identifying restrictions must be supplied. In RISE these are ordinary parameter restrictions on a0 (and, if you wish, on the lag coefficients), passed to estimate in the linear-restrictions cell array – the same mechanism used for block-exogeneity restrictions in the reduced-form VAR chapter.

A recursive (Cholesky-type) identification orders the variables and zeroes the entries of \(A_{0}\) above the diagonal, so that variable 1 is not affected contemporaneously by any other shock, variable 2 only by shock 1, and so on:

linres = {};
for ii = 1:numel(endog)
    for jj = ii+1:numel(endog)
        linres{end+1, 1} = sprintf('a0(%s,%s)=0', endog{ii}, endog{jj}); %#ok<SAGROW>
    end
end

Here endog is the list given to svar_model, and the shock epsilon<k> belongs to the k-th variable of that list. The reduced-form counterpart, identify(m, 'choleski', shock_names) on an rfvar_model declared with the same list, follows the same declaration order and gives the same impulse responses, the k-th shock name standing for epsilon<k> (see Reduced-form VAR Modeling).

Exclusion (zero) restrictions can equally be imposed one at a time, e.g. 'a0(PAIOIL,R)=0' (“the policy-rate shock has no contemporaneous effect on oil-price inflation”), and you can mix them with restrictions on the lag coefficients ('a1(PAIOIL,GROWTH)=0', …).

3.4. Estimation

Estimation is exactly as for a reduced-form VAR – pass the model, a database of time series (see Forecasting and simulation), the estimation sample, an (optional) prior, and the identifying restrictions:

mdlest = estimate(mdl, ...
    data                       = db, ...
    estim_start_date           = date2serial(db.GROWTH.start), ...
    estim_end_date             = date2serial(db.GROWTH.finish), ...
    estim_linear_restrictions  = linres);

Bayesian estimation uses the same var_priors.minnesota factory introduced in Reduced-form VAR Modeling:

var_prior   = var_priors.minnesota(endog, const, exog, nlags, ...
    tightness    = 0.1, ...
    lag_decay    = 1.0, ...
    ar_first_lag = 0.9);

mdlest_bayes = estimate(mdl, ...
    data                       = db, ...
    estim_start_date           = date2serial(db.GROWTH.start), ...
    estim_end_date             = date2serial(db.GROWTH.finish), ...
    estim_var_prior           = var_prior, ...
    estim_linear_restrictions  = linres);

After estimation, inspect the structural form:

print_structural_form(mdlest)

3.5. Reduced-form and structural lag priors

An SVAR’s prior usually has two parts: a prior on \(A_{0}\) (the identifying information, in estim_priors, restrictions and endogenous priors) and a prior that shrinks the lags, typically toward a random walk (in estim_var_prior). RISE offers two kinds of lag prior, and they do not treat your prior on \(A_{0}\) the same way.

Reduced-form priors (minnesota, niw, inw, generate_sims_zha_prior, …) are densities over the reduced form \(B = A_{0}^{-1}[c\;A_{1} \cdots A_{L}]\) and \(\Sigma_{u} = A_{0}^{-1}\mathrm{diag}(\sigma)^{2}A_{0}^{-\prime}\). For an svar_model RISE evaluates them at the reduced form implied by the structural parameters. The result is not a density over the structural parameters \((A_{0}, A_{+}, \sigma)\), and no Jacobian can make it one in general: the change of variables needs a one-to-one map, and \((A_{0}, \sigma)\) has more free parameters than the \(n(n+1)/2\) of \(\Sigma_{u}\) as soon as the SVAR is over-parameterized relative to the reduced form (e.g. 24 against 21 in Ahn and Rudd, 2025). The consequence is not cosmetic: integrating the lags and variances out leaves a factor \(w(A_{0})\) that multiplies the prior you put on \(A_{0}\). You write \(p(A_{0})\); the model uses \(p(A_{0})\,w(A_{0})\). For the Baumeister-Hamilton family the factor is known in closed form (Loria, 2026, Proposition 1); in Ahn and Rudd it moves the posterior mode of \(A_{0}\) by up to 0.36.

Structural priors put the lag prior on the structural coefficients, conditional on \(A_{0}\). Writing equation \(i\) as \(a_{0,i}y_{t} = a_{+,i}x_{t} + \sigma_{i}e_{it}\),

\[a_{+,i}' \mid A_{0}, \sigma \sim N\!\left(P a_{0,i}',\; \sigma_{i}^{2} M_{i}\right), \qquad \sigma_{i}^{-2} \mid A_{0} \sim \mathrm{Gamma}\!\left(\kappa_{i},\; \kappa_{i}\, a_{0,i} S a_{0,i}'\right)\]

(the second block is optional; the Gamma has shape \(\kappa_{i}\) and rate \(\tau_{i} = \kappa_{i} a_{0,i} S a_{0,i}'\), and RISE evaluates it as a density for the parameter \(\sigma_{i} > 0\)). Given \(A_{0}\) this is a proper density over \((A_{+}, \sigma)\): it integrates to one. The marginal prior of \(A_{0}\) is therefore exactly the one you wrote, and there is nothing to correct. They are built by rise.engine.var.var_priors.generate_structural_prior and passed through estim_var_prior like the reduced-form ones:

bh = rise.engine.var.var_priors.generate_structural_prior('bh', ...
        endog, const, exog, nlags, db);          % data: for S

mdlest = estimate(mdl, ...
    data                      = db, ...
    estim_var_prior           = bh, ...
    estim_priors              = a0_priors, ...
    estim_linear_restrictions = linres);

Two families:

  • 'bh' – Baumeister and Hamilton (2015, 2018). Random-walk mean (\(E[A_{1} \mid A_{0}] = A_{0}\); ar_first_lag scales it, mean replaces the whole map \(P\)), \(M_{i}\) diagonal with \(\lambda_{0}^{2}/(l^{2\lambda_{1}}S_{jj})\) for lag \(l\) of variable \(j\) and \(\lambda_{0}^{2}\lambda_{3}^{2}\) for the constant and each deterministic regressor. \(S\) is the covariance matrix of the residuals of univariate AR(nlags) regressions computed from the data you pass (or give S directly). Options lambda0 (0.2), lambda1 (1), lambda3 (100), kappa (2), use_sigma_prior (true): the defaults are Ahn and Rudd’s values.

  • 'sims_zha' – Sims and Zha (1998) in its original structural form. The dummy observations \((Y_{d}, X_{d})\) of the reduced-form Sims-Zha prior (same L1 … L7 and conventions) enter each structural equation as prior observations \(Y_{d}a_{0,i}' = X_{d}a_{+,i}' + \sigma_{i}e\), which stays conjugate: \(M_{i}^{-1} = X_{d}'X_{d}\) (the lag and constant dummies plus the sum-of-coefficients and co-persistence dummies) and \(P = (X_{d}'X_{d})^{-1}X_{d}'Y_{d}\). The Gamma block is the rest of the same dummy likelihood. (Sims and Zha normalize \(\sigma = 1\) with a free scale of \(A_{0}\); RISE’s parameterization with \(\sigma\) free is equivalent.)

The template also returns the pieces the conditional posterior needs (conditional_prior, conditional_posterior: \(G_{i} = X'X + M_{i}^{-1}\), the conditional posterior mean of \(a_{+,i}\) and the residual sum of squares).

When to use which. Use a structural prior whenever the prior on \(A_{0}\) carries the identification – sign restrictions, informative priors on impact coefficients (Baumeister-Hamilton style), endogenous priors on impulse responses – and in any over-identified or set-identified SVAR: it is the only way to get the \(A_{0}\) prior you specify. It is also the prior to use to replicate papers built on the Baumeister-Hamilton or the structural Sims-Zha prior, with no correction term. Reduced-form priors remain the natural choice when the prior is genuinely about the reduced form (forecasting, a shared reduced-form prior across identification schemes) or to reproduce results obtained with them. Even with an exactly identified \(A_{0}\) (e.g. recursive), where a Jacobian would exist, RISE does not apply one, so the reweighting is there too.

What the structural prior covers. It is the prior of the lag, constant and deterministic coefficients, and of each \(\sigma_{i}\) when the Gamma block is on. A prior on any of them in estim_priors would count twice and is an error (setup_priors:StructuralPriorOverlap); with use_sigma_prior = false the \(\sigma_{i}\) take the priors you give them. Priors on \(A_{0}\) are always yours. The parameters it covers still get the wide flat priors RISE creates for every VAR coefficient, so that they are estimated; their contribution is constant.

Defined parameters get no default prior. A parameter that estim_nonlinear_restrictions defines as a function of others ('a0_y_p=-h12/(1-h12*h21)', as in Baumeister-Hamilton-type identification through \(H = A_{0}^{-1}\)) is not free, so RISE gives it none of these flat priors and does not estimate it; for a switching parameter this holds state by state ('a0_y_p(reg,2)=...' leaves a0_y_p_reg_1 free). The order in which the VAR prior and the restrictions are set does not matter. (A prior on such a parameter would make it an estimated coordinate that its definition overwrites: a dead direction of the search and a singular Hessian.)

Regime switching. The prior is evaluated regime by regime, given \(a_{0,i}(r)\) and \(\sigma_{i}(r)\), with two rules that keep it a proper prior over the parameters that actually exist:

  1. One evaluation per distinct parameter. A block (the lags of one equation, or its \(\sigma_{i}\)) that does not switch is evaluated once, not once per regime. (Reduced-form priors evaluate the whole prior once per regime, so a non-switching lag block is counted as many times as there are regimes.)

  2. Mixed switching needs a reference regime. When a block does not switch but its conditioning set does – e.g. only \(\sigma\) switches, so the lag block is conditioned on a switching \(\sigma_{i}\) – the block is conditioned on the reference regime: option reference_regime of the factory, default 1 (the first state of every chain). estimate prints which blocks use it, and the full plan is in mdlest.model_data.estim_priors_data.var_prior_template.structural_plan.

Limits. svar_model only (reduced-form, panel and proxy VARs and DSGE models raise svar_evaluator:StructuralPriorNotSvar); the template’s variables, lags, constant and deterministic terms must match the model’s (svar_evaluator:StructuralLayoutMismatch). With exclusion restrictions on the lags, the Gaussian block is evaluated at the restricted point over the full coefficient vector, as for the reduced-form priors; it is then not the conditional density of the free coefficients. See the tutorial Estimation/svar_structural_priors.

3.5.1. Integrating out the lags: estim_svar_integrate_lags

What it does. Given \(A_{0}\), a structural prior makes the posterior Gaussian in each equation’s lag, constant and deterministic coefficients \(a_{+,i}\) and, with the Gamma block, Gamma in \(\sigma_{i}^{-2}\). Both integrate out in closed form, equation by equation (Loria, 2026, Proposition 2):

\[\log p(A_{0} \mid Y) = \log p(A_{0}) + T\log\lvert\det A_{0}\rvert + \sum_{i}\Big[-\tfrac{1}{2}\log\lvert M_{i}\rvert - \tfrac{1}{2}\log\lvert G_{i}\rvert + \log\Gamma(\kappa_{i} + \tfrac{T}{2}) - \log\Gamma(\kappa_{i}) + \kappa_{i}\log\tau_{i} - (\kappa_{i} + \tfrac{T}{2})\log(\tau_{i} + \tfrac{\zeta_{i}}{2})\Big] - \tfrac{nT}{2}\log 2\pi\]

with \(G_{i} = X'X + M_{i}^{-1}\) and \(\zeta_{i}\) the residual sum of squares of the conditional regression of \(Y a_{0,i}'\) on \(X\), prior term included. \(\log p(A_{0})\) is everything else in RISE’s posterior: your priors on \(A_{0}\) (or on the parameters \(A_{0}\) is derived from), restrictions, endogenous priors. With estim_svar_integrate_lags = true the optimizer (estim_optimizer) searches this marginal posterior, over those parameters only:

mdlest = estimate(mdl, ...
    data                       = db, ...
    estim_var_prior            = bh, ...           % 'bh' or 'sims_zha'
    estim_priors               = a0_priors, ...
    solve_check_stability      = false, ...
    estim_svar_integrate_lags  = true, ...
    estim_svar_start           = 'concentrated');  % optional, recommended

mdlest.model_data.estimation.posterior_maximization.svar_integrated

At the mode, the lags and \(\sigma\) are set to their conditional posterior mode given \(A_{0}\) (the joint mode of \((a_{+,i}, \sigma_{i})\), with \(\sigma_{i}^{2} = (2\tau_{i} + \zeta_{i})/(T + k + 2\kappa_{i} + 1)\)), so the estimated model is complete and irf, filter, forecast work as usual. The summary svar_integrated holds the log marginal posterior at the mode (log_marginal_posterior; the joint kernel there is the usual log_post), the log marginal likelihood \(\log p(Y \mid A_{0})\), the parameters searched and integrated, the Hessian of the marginal and a Laplace marginal data density built on it (also reported as log_marginal_data_density_laplace). The standard errors of the integrated coefficients combine their conditional posterior covariance given \(A_{0}\) with the uncertainty about \(A_{0}\) (law of total variance, to first order). With estim_svar_start = 'concentrated' the multi-start search runs on the same marginal posterior; use it, the marginal can have local modes. The marginal can be evaluated at any point with rise.engine.var.vartools.svar_integrated_posterior.

Why it helps. The search runs over \(A_{0}\) alone (18 parameters instead of 318 in Ahn and Rudd). And in Baumeister-Hamilton type models, where the identifying information is on \(H = A_{0}^{-1}\) and each column of \(H\) carries a unit normalization, the joint posterior over all parameters has no mode: when a column of \(H\) grows, the matching row of \(A_{0}\) shrinks, \(\sigma_{i}\) and the lag block shrink with it, and the joint density maximized over \((A_{+}, \sigma)\) grows like \(\lvert h\rvert^{k+1}\), faster than a Student-t prior on \(h\) pushes back. The joint optimizer runs off to infinity; the marginal posterior of \(A_{0}\), which is what Baumeister and Hamilton (and Ahn and Rudd) work with, has a mode. With the 'bh' structural prior and this option, such models need neither a correction term nor a custom mode search (the comparison with Ahn and Rudd’s own numbers is still to be run). See the tutorial Estimation/svar_integrated_lags.

When it qualifies. Otherwise estimate stops with an error; it never falls back silently to the joint search.

  • svar_model with constant parameters. Under regime switching the likelihood mixes over regime paths and the integral is not closed-form (estimate:svarIntegrateSwitching); an EM profile search and a Gibbs sampler are planned.

  • A structural prior in estim_var_prior (estimate:svarIntegrateNoStructuralPrior), and no other prior on the lags or on a \(\sigma\) it covers (setup_priors:StructuralPriorOverlap, estimate:svarIntegrateOverlap).

  • No endogenous prior involving the lags or \(\sigma\), including impulse responses and moments, and no endogenous prior given as a function handle, which cannot be checked (estimate:svarIntegrateEndogenousPriors). Endogenous priors on \(A_{0}\) parameters are fine.

  • Linear restrictions: exclusion restrictions on the lags (and lags tied to \(A_{0}\)) are integrated exactly over the free coefficients – the closed form above with \(G_{i} = D'(X'X + M_{i}^{-1})D\) and \(\kappa_{i} + (T + k - f_{i})/2\) in place of \(\kappa_{i} + T/2\), \(f_{i}\) the number of free coefficients. Restrictions tying lags of different equations, or a lag to \(\sigma\), a fixed or restricted \(\sigma\) under the Gamma block, lags in estim_nonlinear_restrictions, derived lags and estim_general_restrictions are refused (estimate:svarIntegrateRestrictions).

  • solve_check_stability = false: the stability check truncates the region of the lags that the closed form integrates over (estimate:svarIntegrateOptions, as are estim_mle, estim_eval_lik = false, estim_blocks and missing data).

Without the Gamma block (use_sigma_prior = false) only the lags are integrated out; each \(\sigma_{i}\) is searched with \(A_{0}\) under its own prior, and the closed form is \(-\tfrac{T}{2}\log 2\pi - \tfrac{1}{2}\log\lvert M_{i}\rvert - \tfrac{1}{2}\log\lvert G_{i}\rvert - T\log\sigma_{i} - \zeta_{i}/(2\sigma_{i}^{2})\) per equation.

Limits. Constant parameters only for now. The Laplace marginal data density is an approximation over \(A_{0}\) only (the lags and variances are integrated exactly). Parameters that estim_nonlinear_restrictions define get no default prior and are not searched. An estimated parameter that still moves no model parameter – e.g. an \(A_{0}\) entry given a prior in estim_priors although a definition overwrites it – is reported as inert and stays at its start value (its posterior is its prior).

3.6. Starting values: estim_svar_start

The posterior-mode search starts, by default, from the prior start values: \(A_{0} = I\), and whatever the priors (or the VAR prior’s defaults) say for the lag coefficients, constants and shock standard deviations; parameters with neither a prior nor a value get the reduced-form OLS estimates under \(A_{0} = I\). This is estim_svar_start = 'ols_identity', the default. That point can be far from the mode, and it need not satisfy the identifying restrictions on \(A_{0}\).

estim_svar_start = 'concentrated' starts somewhere better. Given \(A_{0}\), the other blocks have closed-form conditional maximum-likelihood values, from the OLS reduced form \((\hat{B}, \hat{c}, \hat{\Omega})\) over the estimation sample:

\[A_{l} = A_{0}\hat{B}_{l}, \qquad c = A_{0}\hat{c}, \qquad \sigma_{i}^{2} = \left(A_{0}\hat{\Omega}A_{0}'\right)_{ii}\]

Substituting them leaves a posterior in \(A_{0}\) alone – with every prior, linear and nonlinear restriction, derived parameter and endogenous prior, since RISE evaluates it with its own posterior kernel. RISE maximizes it over the estimated \(A_{0}\) parameters from estim_svar_start_trials starting points (default 10): the prior start values, then draws from the \(A_{0}\) priors that satisfy the restrictions. The best point goes to the optimizer:

mdlest = estimate(mdl, ...
    data                         = db, ...
    estim_var_prior              = var_prior, ...
    estim_linear_restrictions    = linres, ...
    estim_svar_start             = 'concentrated', ...
    estim_svar_start_trials      = 10);

mdlest.model_data.estimation.posterior_maximization.svar_start

The summary svar_start records the log posterior at the chosen start and at the 'ols_identity' one, the \(A_{0}\) parameters searched over, and the number of posterior evaluations the search cost (not included in funevals, which counts the optimizer’s).

What it guarantees, and what it does not:

  • Only estimated parameters move. Parameters without a prior keep their values (yours, or the 'ols_identity' seed). Linear restrictions are imposed after the lag, constant and sigma blocks are filled in, so a restricted lag coefficient keeps its restricted value.

  • Never worse than the default. The 'ols_identity' start is evaluated too, and kept when its posterior is higher.

  • It is a start, not an estimate. With priors or restrictions on the lags, the conditional ML blocks are not their conditional posterior modes; the optimizer takes it from there.

  • Cost. The search runs over the free \(A_{0}\) entries only (a Nelder-Mead search per starting point), whatever the number of lag coefficients. On a small model it can cost more evaluations than it saves the optimizer; it pays off as the number of lag parameters grows, and above all when the default start leads to a local mode.

  • svar_model only. On reduced-form, panel or proxy VARs (and DSGE models) 'concentrated' is an error.

  • With estim_svar_integrate_lags the same multi-start search runs on the integrated posterior of \(A_{0}\) instead (the lags and variances integrated out rather than set to their conditional ML values); svar_start.integrated is then true.

Under regime switching the start is a heuristic. The constant-parameter fit above is computed with every regime tied to the same parameters, then copied to every regime; the switching blocks are perturbed (each shock standard deviation scaled down in the low states and up in the high states of its chain, switching coefficients by a few percent), the transition probabilities stay at their prior start values, and the best of estim_svar_start_trials perturbations goes to the optimizer. There is no EM step yet to pull the regimes apart before the optimizer does. On a SVAR with volatility regimes, the default start can end in a local mode far below the one this start reaches (see the tutorial Estimation/svar_concentrated_start).

3.7. Impulse responses, decompositions, forecasting

Because the model is already structural, no identification function is needed: the shock names are simply the structural shocks (one per endogenous variable, by RISE’s naming convention), and irf / variance_decomposition / historical_decomposition / forecast are called directly:

myirfs = irf(mdlest);                          % all shocks, default horizon
myirfs = irf(mdlest, shock_names, 40);

vd     = variance_decomposition(mdlest);

hd     = historical_decomposition(mdlest);

fkst   = forecast(mdlest, db, date2serial('2003Q1'));

For plotting (quick_irfs, plot_fanchart, plot_decomp, …), parameter uncertainty via bootstrap, Bayesian posterior sampling, fan charts of the decompositions and IRFs, and conditional forecasting, see Reduced-form VAR Modeling – the calls are identical, with a0 / a1 / … parameters in place of the reduced-form b1 / b2 / … ones, and without the extra Rfunc (identification) argument.

3.8. Adding regime switching

Pass a Markov-chain structure to the factory and list the parameters it controls – for an SVAR these are typically a0 (switching contemporaneous transmission) and/or a1, …, ap:

mc                          = struct();
mc.name                     = 'policy';
mc.number_of_states         = 2;
mc.controlled_parameters    = {'a0(R,:)'};
mc.endogenous_probabilities = [];
mc.probability_parameters   = [];

mdl = svar_model(endog, ...
    lag_length         = nlags, ...
    constant_term      = const, ...
    deterministic_vars = exog, ...
    markov_chains      = mc);

Time-varying transition probabilities are specified exactly as in Reduced-form VAR Modeling (an endogenous_probabilities definition plus the probability_parameters that enter it), and the switching parameters are given priors through estim_priors.

Proxy / instrumental SVARs are handled by the related Proxy (instrumental) SVAR Modeling object, and panels of (structural) VARs by the Panel VAR Modeling object.