---
title: "A forecast in ten minutes: the canonical QPM in qpmR"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{A forecast in ten minutes: the canonical QPM in qpmR}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>",
                      fig.width = 7, fig.height = 5)
```

qpmR implements the class of semi-structural quarterly projection models
(QPM) used in central-bank Forecasting and Policy Analysis Systems: an
IS curve, a hybrid Phillips curve, a forward-looking inflation-targeting
rule, and an exchange-rate block, solved under model-consistent
expectations.

## The canonical model

`qpm_template("bkl")` ships the canonical Berg–Karam–Laxton (IMF
WP/06/80–81) small open economy model with an illustrative
emerging-economy calibration — a 5 percent inflation target and a
positive country risk premium.

```{r}
library(qpmR)
m <- qpm_template("bkl")
m
```

The policy rule responds to expected year-on-year inflation four
quarters ahead, `E(pi4[+4])`; the four-quarter identity uses lags back
to `pi[-3]`. qpmR turns those long leads and lags into auxiliary states
automatically when the model is solved.

## Solving and checking

```{r}
sol <- qpm_solve(m)
sol
```

The Blanchard–Kahn line is a real diagnostic: an indeterminate or
explosive model refuses to solve and the error names the usual economic
cause. Violating the Taylor principle, for instance:

```{r, error = TRUE}
qpm_solve(qpm_calibrate(m, c2 = -0.5))
```

## Monetary transmission

```{r}
ir <- irf(sol, shock = "eps_i", horizon = 16)
ir
plot(ir, vars = c("i", "r", "pi", "y_gap", "q", "pi4"))
```

A policy tightening raises the real rate, opens a negative output gap,
appreciates the currency on impact (UIP), and produces the hump-shaped
disinflation with a mild rebound as policy later eases below neutral —
the standard QPM transmission story.

## History and forecast

Until the Kalman filter arrives in 0.2, initial states come from
simulation:

```{r}
histq <- simulate(sol, nsim = 48, seed = 7, burn = 20)
fc <- qpm_forecast(sol, from = histq, horizon = 12)
fc
plot(fc, vars = c("pi", "i", "y_gap", "q"))
```

The fan bands are analytic, from the forecast-error variance recursion
`V_h = P V_{h-1} P' + Q S Q'`.

## Specification checks

```{r}
qpm_lint(m)
```

## Adapting the model to your country

Real engagements never use the canonical model unchanged. Extension
blocks make that adaptation reusable and reviewable instead of a fork.

The most common adaptation is disaggregating the CPI. Food is 30-50
percent of the basket across most of sub-Saharan Africa and South Asia,
and a single-inflation model cannot represent the policy question there
-- supply shocks to food dominate headline, but policy should look
through the relative-price component:

```{r}
m_food <- add_block(qpm_template("bkl"), block_food_cpi(weight = 0.45))
plot(irf(qpm_solve(m_food), shock = "eps_pifood", horizon = 16),
     vars = c("pi_food", "pi", "pi_core", "i"))
```

Food inflation jumps, headline follows scaled by the basket weight, and
core barely moves -- so the policy response stays small. That is the
model saying what a good desk would say.

Exchange-rate management is the other common adaptation. One
`intensity` argument spans the regimes: zero reproduces the free float
exactly, larger values approach a peg.

```{r}
peak_q <- function(m) {
  ir <- irf(qpm_solve(m), shock = "eps_prem", horizon = 12, size = 1)
  max(abs(ir$value[ir$variable == "q"]))
}
c(float   = peak_q(qpm_template("bkl")),
  managed = peak_q(add_block(qpm_template("bkl"), block_fx_intervention(1))),
  peg     = peak_q(add_block(qpm_template("bkl"), block_fx_intervention(4))))
```

Whatever a country team changes, `qpm_diff()` makes it reviewable:

```{r}
qpm_diff(qpm_template("bkl"), m_food)
```

## Real data: Czechia

qpmR ships `czechia`, a quarterly dataset from 1996 in the model's units
(see `?czechia`). With `trends = "rw"` the equilibrium real exchange
rate and potential growth become random walks — the model then has unit
roots and `qpm_filter()` switches to diffuse initialization
automatically.

```{r}
mcz <- qpm_calibrate(qpm_template("bkl", trends = "rw"),
                     pi_tar = 2, istar_ss = 2, pistar_ss = 2,
                     prem_ss = 1, a5 = 0.4)
cz <- czechia[czechia$period >= "1999",
              c("period", "pi4", "i", "q", "dy_obs", "istar", "pistar")]
fit <- qpm_filter(mcz, cz)
fit
```

The smoothed latent states reproduce the known history — the pre-GFC
boom, the 2009 and 2013 recessions, the COVID crater, the trend real
appreciation of the koruna, and the post-GFC fall in potential growth:

```{r}
plot(fit, vars = c("y_gap", "dy_bar", "r_bar", "q_gap"))
```

Historical shock decomposition of the 2021-23 inflation wave:

```{r}
dec <- qpm_decompose(fit)
plot(dec, var = "pi4", periods = (fit$n_obs - 33):fit$n_obs)
```

And a forecast from the smoothed end-of-sample state:

```{r}
fc <- qpm_forecast(qpm_solve(mcz), from = fit, horizon = 12)
plot(fc, vars = c("pi4", "i", "y_gap", "q"))
```

Note the honesty of the diagnostics on real data: the Ljung-Box test
flags autocorrelated exchange-rate innovations (the driftless `q_bar`
random walk absorbs the appreciation trend through its shocks), and the
2022-23 inflation surprises show up as large outliers. That is the
model asking for judgment and recalibration — which is what 0.3 is for.

## Policy analysis: conditions, judgment, rounds

A forecast becomes policy analysis when you can impose assumptions on
it. `qpm_condition()` inverts the model for the shocks behind any
assumed path -- with an explicit `anticipated` switch, because an
announced rate path and a sequence of surprises are different economics:

```{r}
base <- qpm_forecast(qpm_solve(mcz), from = fit, horizon = 12)
hold <- qpm_condition(base,
                      i = stats::setNames(rep(3.5, 4), base$periods[1:4]),
                      anticipated = TRUE, instruments = "eps_i")
hold
```

Judgment is a logged, auditable operation rather than a spreadsheet
tweak:

```{r}
judged <- add_judgment(base, pi4 = stats::setNames(0.4, base$periods[3]),
                       author = "prices desk",
                       rationale = "announced energy-tariff increase")
judgment_log(judged)
```

A forecast round archives the whole pipeline -- model, calibration, data
vintage, filtration, conditioned forecast -- as one replayable object,
and `compare_rounds()` answers the question every chief economist asks:
*why did the forecast move?*

```{r}
czA <- cz[cz$period <= "2025-Q4", ]
rA <- qpm_round("2026-Q1 March", mcz, czA, horizon = 12)
rB <- qpm_round("2026-Q3 September", mcz, cz, horizon = 12)
rB <- add_judgment(rB, pi4 = c("2027-Q1" = 0.4), author = "prices desk",
                   rationale = "announced energy-tariff increase")
rev <- compare_rounds(rA, rB, variables = c("pi4", "i", "y_gap"))
print(rev, variables = "pi4", periods = "2027-Q1")
plot(rev, variable = "pi4")
```

The contributions telescope, so they sum to the total revision exactly,
and the decomposition endpoints are verified against the archived
rounds. `save_round()` / `load_round()` / `list_rounds()` manage the
on-disk round store, with human-readable CSV sidecars for auditing
without R.

## Closing the round: audit and report

An archived round is only useful if it still reproduces. `verify_round()`
re-runs the whole pipeline from the round's own contents — model,
calibration, data vintage, conditions and judgment — and checks the
published numbers against the archive:

```{r}
verify_round(rB)
```

A round that no longer reproduces is a finding, not a mystery: the
report names the largest deviation, the variables responsible, and any
qpmR version drift. Loaded from a store, it also checks the CSV
sidecars against the object, so a hand-edited audit trail shows up.

The round then becomes the deliverables a policy meeting runs on — the
standard chart pack and the monetary policy report, which carries the
executive summary, the fan charts, the gaps, the shock decomposition,
the judgment ledger and the revision against the previous round:

```{r, eval = FALSE}
chart_pack(rB, "chart_pack.pdf")
qpm_report(rB, "mpr.html", compare_to = rA)
```

`qpm_report()` always writes the `.Rmd` source next to its output, on
the principle that institutions replace the template's *text*, not its
plumbing.

## Where to go next

Estimation — priors, posterior sampling on the filter likelihood,
identification diagnostics, marginal likelihoods, and posterior fan
charts — is covered in `vignette("qpmR-estimation")`. The policy
experiments build on the same objects: `qpm_rule_eval()` scores
alternative rules on the inflation-output variability frontier,
`qpm_counterfactual()` replays history with shocks switched off,
`qpm_compare_models()` sets two models' impulse responses and moments
side by side, and `fevd()` and `model_properties()` report which shocks
drive which variables. `write_dynare()` exports any model as a `.mod`
file for an independent check in Dynare.
