About this note: This document presents the foundations of the Small-Scale Model (MPP) developed by the Central Bank of Brazil to support COPOM decisions during the early years of the inflation-targeting regime (1999). The structure follows Bogdanski, Tombini & Werlang (2000), with pedagogical extensions for estimation in R. The goal is twofold: to document the model with rigor and to serve as an accessible reference for macroeconomic analysts.

1 Motivation: Why Small-Scale Models?

The inflation-targeting regime requires the central bank to adopt a forward-looking stance: monetary policy decisions affect inflation with considerable lags. As Greenspan put it:

“Implicit in any monetary policy action or inaction is an expectation of how the future will unfold, that is, a forecast.”

Large-scale models (full DSGEs, high-dimensional VARs) have desirable properties in terms of micro-foundations or predictive capacity, but they are opaque for communication with policymakers. The MPP occupies a strategic position in the model hierarchy:

The MPP is small enough to be explained to the COPOM and structured enough to simulate alternative monetary policy scenarios.


2 Model Structure

The MPP consists of four main blocks, articulated as shown in the diagram below:


3 Block I — IS Curve (Aggregate Demand)

3.1 Theoretical Specification

The IS Curve relates the output gap to its own lagged values, the real interest rate, and optionally the primary fiscal balance.

Baseline specification:

\[ \boxed{\tilde{y}_t = \beta_0 + \beta_1 \tilde{y}_{t-1} + \beta_2 \tilde{y}_{t-2} + \beta_3 r_{t-1} + \varepsilon^d_t} \]

where \(\tilde{y}_t = \ln(Y_t / Y^*_t)\) is the output gap and \(r_t = \ln(1 + R_t)\) is the real interest rate.

With fiscal variable (“fiscal IS” specification):

\[ \tilde{y}_t = \beta_0 + \beta_1 \tilde{y}_{t-1} + \beta_2 \tilde{y}_{t-2} + \beta_3 r_{t-1} + \underbrace{\phi \cdot \text{NFPS}_{t-1}}_{\text{fiscal impulse}} + \varepsilon^d_t \]

where \(\text{NFPS}_{t-1}\) is the Net Financing Need of the Public Sector (primary concept, % of GDP).

3.2 Long-Run Calibration

Long-run restriction: In steady state, the output gap is zero, the primary surplus is zero, and the debt-to-GDP ratio is constant. This implies that the equilibrium real rate must equal the potential GDP growth rate:

\[ r^* = g^* = -\dfrac{\beta_0}{\beta_3} \]

This restriction is imposed in estimation as a linear constraint on parameters \((\beta_0, \beta_3)\).

3.3 Output Gap Estimation

The gap is obtained as the difference between observed and potential GDP. The original model used the Hodrick-Prescott (HP) filter and a linear deterministic trend. We reproduce the exercise below with simulated data:

# ── Simulated data with structure analogous to 1995–2024 ──
set.seed(42)
n     <- 120   # quarters
trend <- cumsum(c(0.005, rep(0.006, n-1) + rnorm(n-1, 0, 0.001)))

# Generate cycle iteratively
cycle_v <- numeric(n)
for(t in 2:n) cycle_v[t] <- 0.65 * cycle_v[t-1] + rnorm(1, 0, 0.01)

ln_gdp <- 100 + trend + cycle_v
dates  <- seq(as.Date("1995-01-01"), by = "quarter", length.out = n)

df_gdp <- data.frame(date = dates, ln_gdp = ln_gdp)

# HP filter (lambda = 1600 for quarterly data)
hp_result        <- mFilter::hpfilter(df_gdp$ln_gdp, freq = 1600)
df_gdp$potential <- hp_result$trend
df_gdp$gap       <- hp_result$cycle

# Chart 1: observed vs. potential GDP
p1 <- ggplot(df_gdp, aes(x = date)) +
  geom_line(aes(y = ln_gdp,    color = "Observed GDP"), linewidth = 1) +
  geom_line(aes(y = potential, color = "Potential GDP (HP)"), linewidth = 1.2,
            linetype = "dashed") +
  scale_color_manual(values = c("Observed GDP" = "#0f3460",
                                "Potential GDP (HP)" = "#e94560"),
                     name = "") +
  labs(title    = "Observed GDP vs. Potential Output",
       subtitle = "Hodrick-Prescott filter (λ = 1,600)",
       x = NULL, y = "Index (log)",
       caption = "Note: simulated data for pedagogical purposes") +
  theme_mpp

# Chart 2: output gap
p2 <- ggplot(df_gdp, aes(x = date, y = gap)) +
  geom_hline(yintercept = 0, color = "#888", linetype = "dotted") +
  geom_area(fill = "#0f3460", alpha = 0.15) +
  geom_line(color = "#0f3460", linewidth = 1.1) +
  labs(title    = "Output Gap",
       subtitle = "Percentage deviation of GDP from potential",
       x = NULL, y = "%",
       caption = "Source: own elaboration") +
  scale_y_continuous(labels = scales::percent_format(scale = 1)) +
  theme_mpp

p1 / p2
Quarterly GDP and potential output estimate via HP filter

Quarterly GDP and potential output estimate via HP filter

# ── IS Curve estimation by OLS with robust standard errors ──

# Constructing simulated real interest rate (ex-post)
set.seed(7)
nominal_rate <- 0.18 + 0.3 * (-df_gdp$gap) + rnorm(n, 0, 0.03)
inflation_sim <- 0.06 + 0.5 * df_gdp$gap + rnorm(n, 0, 0.015)
real_rate     <- nominal_rate - inflation_sim

df_is <- df_gdp %>%
  mutate(
    y    = gap,
    y_l1 = lag(gap, 1),
    y_l2 = lag(gap, 2),
    r_l1 = lag(real_rate, 1)
  ) %>%
  drop_na()

model_is <- lm(y ~ y_l1 + y_l2 + r_l1, data = df_is)

# Robust (HAC) standard errors — Newey-West
errors_hac  <- sandwich::NeweyWest(model_is, lag = 4, prewhite = FALSE)
coeftest_is <- lmtest::coeftest(model_is, vcov = errors_hac)

knitr::kable(
  broom::tidy(coeftest_is) %>%
    rename(Parameter = term, Estimate = estimate,
           `Std. Error` = std.error, `t-stat` = statistic,
           `p-value` = p.value) %>%
    mutate(
      Parameter = c("β₀ (intercept)", "β₁ (ỹ_{t-1})", "β₂ (ỹ_{t-2})", "β₃ (r_{t-1})"),
      across(where(is.numeric), ~ round(.x, 4))
    ),
  caption = "**IS Curve — OLS with HAC errors (Newey-West, 4 lags)**",
  align   = "lrrrr"
) %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
                            full_width = FALSE)
IS Curve — OLS with HAC errors (Newey-West, 4 lags)
Parameter Estimate Std. Error t-stat p-value
β₀ (intercept) -0.0036 0.0030 -1.2079 0.2296
β₁ (ỹ_{t-1}) 0.5020 0.0859 5.8443 0.0000
β₂ (ỹ_{t-2}) -0.1182 0.0782 -1.5120 0.1333
β₃ (r_{t-1}) 0.0276 0.0239 1.1540 0.2509
df_is$fitted <- fitted(model_is)
df_is$resid  <- residuals(model_is)

p_fit <- ggplot(df_is, aes(x = date)) +
  geom_line(aes(y = y * 100,      color = "Observed"),  linewidth = 1) +
  geom_line(aes(y = fitted * 100, color = "Fitted"),    linewidth = 1, linetype = "dashed") +
  scale_color_manual(values = c("Observed" = "#0f3460", "Fitted" = "#e94560"), name = "") +
  labs(title = "Observed vs. fitted output gap", x = NULL, y = "Gap (%)") +
  theme_mpp

p_res <- ggplot(df_is, aes(x = date, y = resid * 100)) +
  geom_hline(yintercept = 0, linetype = "dotted", color = "#888") +
  geom_line(color = "#1a7abf", linewidth = 0.9) +
  labs(title = "IS Curve residuals", x = NULL, y = "Residual (p.p.)") +
  theme_mpp

p_fit / p_res
IS Curve diagnostics

IS Curve diagnostics


4 Block II — Phillips Curve (Aggregate Supply)

4.1 The Three Specifications

The MPP Phillips Curve takes three forms, differing in the expectations formation mechanism. In all of them, the sum of inflation coefficients is restricted to unity (long-run neutrality).

4.1.1 Backward-Looking Specification (Purely Adaptive)

\[ \pi_t = \alpha^b_1 \pi_{t-1} + \alpha^b_2 \pi_{t-2} + \alpha^b_3 \tilde{y}_{t-1} + \alpha^b_4 \Delta \ln E_t + \varepsilon^b_t \]

with \(\alpha^b_1 + \alpha^b_2 = 1\) (long-run vertical Phillips curve).

4.1.2 Forward-Looking Specification (Expectations-Based)

\[ \pi_t = \alpha^f_1 \pi_{t-1} + \alpha^f_2 \mathbb{E}_t[\pi_{t+1}] + \alpha^f_3 \tilde{y}_{t-1} + \alpha^f_4 \Delta \ln E_t + \varepsilon^f_t \]

4.1.3 Hybrid Specification (BCB Preferred)

\[ \boxed{\pi_t = \frac{\alpha^b_1 + \alpha^f_1}{2}\pi_{t-1} + \frac{\alpha^b_2}{2}\pi_{t-2} + \frac{\alpha^f_2}{2}\mathbb{E}_t[\pi_{t+1}] + \alpha^n_3 \tilde{y}_{t-1} + \alpha^n_4 \Delta \ln E_t + \varepsilon^n_t} \]

Why the hybrid specification? The purely forward-looking version generates dynamics with almost no inflationary inertia — incompatible with the Brazilian reality. The purely backward-looking version is vulnerable to the Lucas critique. The hybrid specification balances inflationary persistence (fundamental in Brazil given the history of indexation) with a forward-looking component that gains weight as regime credibility consolidates.

4.2 Exchange Rate Pass-Through

The coefficient \(\alpha_4\) captures the pass-through from exchange rate changes (\(\Delta \ln E_t\)) to inflation. The MPP tested four specifications:

Specification Formula for \(\alpha_4\) Interpretation
Constant linear \(\alpha_4 = c\) Fixed pass-through
Quadratic in \(\Delta e\) \(\alpha_4 = a_{41} + a_{42}(\Delta \ln E)^2\) Asymmetry: large shocks have higher pass-through
Level-dependent \(\alpha_4 = a_{41} + a_{42} \ln E_{t-1}\) Exchange rate level affects pass-through
Quadratic in level \(\alpha_4 = a_{41} + a_{42}\frac{(E-a_{42}/2)^2}{a_{42}^2/4}\) Non-linearity around equilibrium

Empirical evidence from Goldfajn & Werlang (1999) suggests that pass-through is inversely proportional to the degree of real currency appreciation prior to the depreciation episode.

# ── Hybrid Phillips Curve simulation ──
set.seed(21)
alpha1    <- 0.45;  alpha2    <- 0.20; alpha3 <- 0.25
alpha4    <- 0.12;  alpha_fwd <- 0.35

delta_e <- rnorm(n, 0, 0.04)   # exchange rate change (log)
eps_pc  <- rnorm(n, 0, 0.008)  # supply shock

pi <- numeric(n)
pi[1:3] <- 0.06
for (t in 4:n) {
  pi_fwd <- pi[t-1] + rnorm(1, 0, 0.005)
  pi[t]  <- alpha1 * pi[t-1] + alpha2 * pi[t-2] +
            alpha_fwd * pi_fwd +
            alpha3 * df_gdp$gap[t] +
            alpha4 * delta_e[t] + eps_pc[t]
  pi[t]  <- max(pi[t], -0.02)
}

df_pc <- data.frame(
  date     = dates,
  inflation = pi,
  gap      = df_gdp$gap,
  delta_e  = delta_e
)

p_pi <- ggplot(df_pc, aes(x = date, y = inflation * 100)) +
  geom_line(color = "#e94560", linewidth = 1.1) +
  geom_hline(yintercept = 4.5, linetype = "dashed", color = "#0f3460", linewidth = 0.8) +
  annotate("text", x = dates[10], y = 4.9, label = "Inflation target (4.5%)",
           color = "#0f3460", size = 3.5) +
  labs(title    = "Simulated inflation — Hybrid Phillips Curve",
       subtitle = "Backward + forward-looking dynamics",
       x = NULL, y = "CPI (% y/y)",
       caption  = "Simulated data with illustrative parameters") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  theme_mpp

# ── Exchange rate pass-through: alternative specifications ──
E_seq <- seq(1.5, 6.5, length.out = 100)

df_pt <- data.frame(
  E         = E_seq,
  Constant  = rep(0.12, 100),
  Quadratic = 0.08 + 0.04 * (log(E_seq))^2,
  Level     = pmax(0.05, 0.22 - 0.03 * log(E_seq))
) %>%
  pivot_longer(
    cols      = c(Constant, Quadratic, Level),
    names_to  = "Specification",
    values_to = "coef"
  ) %>%
  mutate(Specification = recode(Specification,
    Constant  = "Constant pass-through",
    Quadratic = "Quadratic pass-through",
    Level     = "Level-dependent"
  ))

p_pt <- ggplot(df_pt, aes(x = E, y = coef, color = Specification)) +
  geom_line(linewidth = 1.1) +
  scale_color_manual(values = c("#0f3460", "#e94560", "#2ab7ca")) +
  labs(title    = "Exchange rate pass-through: alternative specifications",
       subtitle = "Sensitivity of the coefficient to the exchange rate level",
       x        = "Exchange Rate (BRL/USD)", y = "Coefficient α₄",
       caption  = "Source: adapted from Goldfajn & Werlang (1999)") +
  theme_mpp +
  theme(legend.title = element_blank())

p_pi / p_pt
Hybrid Phillips Curve — Fit and Pass-Through

Hybrid Phillips Curve — Fit and Pass-Through


5 Block III — Uncovered Interest Parity (UIP)

5.1 Formulation

The nominal exchange rate is determined by the uncovered interest parity condition:

\[ \mathbb{E}_t[e_{t+1}] - e_t = i_t - i^*_t - x_t \]

where \(e_t = \ln E_t\) (log of the exchange rate), \(i_t\) is the domestic rate (SELIC), \(i^*_t\) is the foreign rate, and \(x_t\) is the country risk premium.

Taking first differences and assuming the change in expectations follows a white-noise process:

\[ \boxed{\Delta e_t = \Delta i_t + \Delta x_t - \Delta i^*_t + \eta_t} \]

5.2 Endogenizing the Country Risk Premium

The risk premium can be modeled as a function of fiscal variables and global liquidity:

\[ \Delta x_t = \gamma_1 \Delta x_{t-1} + \gamma_2 \Delta x_{t-2} - \gamma_3 \Delta \text{NFPS}_{t-1} + \sum_j \gamma_j \Delta Z_{j,t} + u_t \]

where \(Z_j\) includes international liquidity conditions, foreign asset performance, commodity prices, and sovereign ratings.

set.seed(99)
i_ext   <- 0.055
x       <- numeric(n)
delta_i <- numeric(n)
e       <- numeric(n)

x[1] <- 300   # bps
e[1] <- log(3.5)

for(t in 2:n) {
  # Country risk premium: AR(1) + fiscal shock
  x[t] <- 0.7 * x[t-1] + rnorm(1, 0, 25) - 15 * (t > 60)
  x[t] <- max(x[t], 50)
  # Domestic rate (annualized nominal SELIC, in log)
  selic_t <- 0.12 - 0.02*(t/n) + rnorm(1, 0, 0.015)
  # Exchange rate change: simplified UIP
  e[t] <- e[t-1] + (selic_t - i_ext - x[t]/10000) / 4 + rnorm(1, 0, 0.025)
}

df_uip <- data.frame(
  date   = dates,
  fx     = exp(e),
  risk   = x
)

p_e <- ggplot(df_uip, aes(x = date)) +
  geom_line(aes(y = fx), color = "#0f3460", linewidth = 1.1) +
  labs(title    = "Simulated exchange rate (BRL/USD)",
       subtitle = "Dynamics via UIP condition with endogenous risk premium",
       x = NULL, y = "BRL/USD") +
  theme_mpp

p_x <- ggplot(df_uip, aes(x = date)) +
  geom_line(aes(y = risk), color = "#e94560", linewidth = 1) +
  geom_hline(yintercept = 200, linetype = "dashed", color = "#888") +
  labs(title    = "Country risk premium (EMBI+)",
       subtitle = "AR(1) process with gradual fiscal improvement",
       x = NULL, y = "Basis Points (bps)") +
  theme_mpp

p_e / p_x
Exchange rate dynamics simulated by the UIP condition

Exchange rate dynamics simulated by the UIP condition


6 Block IV — Monetary Policy Rule

6.1 Rule Families

The MPP allows three families of rules for the policy rate:

6.1.1 1. Pre-defined Exogenous Path

\[ i_t = \bar{i}_t \quad (\text{exogenous path}) \]

Useful for simulating baseline scenarios or paths implied by the market yield curve. This is the rule displayed in the Inflation Report fan chart under the “constant policy rate” assumption.

6.1.2 2. Taylor Rule (with Smoothing)

\[ \boxed{i_t = (1-\lambda)\,i_{t-1} + \lambda \left[\alpha_1(\pi_t - \pi^*) + \alpha_2 \tilde{y}_t + \alpha_3\right]} \]

  • \(\lambda = 1\): pure Taylor rule
  • \(\lambda \in (0,1)\): Taylor with interest rate smoothing
  • The \(\alpha_i\) can be set arbitrarily or optimized

6.1.3 3. Forward-Looking Rule

\[ i_t = (1-\lambda)\,i_{t-1} + \lambda \left[\alpha_1(\mathbb{E}_t[\pi_{t+h}] - \pi^*) + \alpha_2 \tilde{y}_t\right] \]

where \(h\) is the projection horizon (typically 4–8 quarters).

6.1.4 4. Optimal Stochastic Rule

The optimal rule minimizes the intertemporal quadratic loss function:

\[ \mathcal{L} = \mathbb{E}_t \sum_{r=1}^{N} \left[\lambda_1(\pi_{t+r} - \pi^*)^2 + \lambda_2 \tilde{y}_{t+r}^2 + \lambda_3(\Delta i_{t+r})^2\right] \]

The third term (\(\lambda_3 \Delta i^2\)) penalizes SELIC volatility, reflecting the operational objective of smoothing. By the certainty equivalence principle (for linear models), the deterministic and stochastic cases are equivalent in expectation.

set.seed(55)

# Illustrative parameters
alpha_pi  <- 1.5   # weight on inflation (Taylor)
alpha_y   <- 0.5   # weight on output gap
lambda_sm <- 0.7   # smoothing parameter
pi_star   <- 0.045

N_rules  <- 80
pi_sim   <- pi[1:N_rules]
y_sim    <- df_gdp$gap[1:N_rules]

# Rule 1: pure Taylor
i_taylor <- alpha_pi * (pi_sim - pi_star) + alpha_y * y_sim + 0.10

# Rule 2: Taylor with smoothing
i_smooth    <- numeric(N_rules)
i_smooth[1] <- i_taylor[1]
for (t in 2:N_rules)
  i_smooth[t] <- (1 - lambda_sm) * i_smooth[t-1] + lambda_sm * i_taylor[t]

# Rule 3: constant policy rate (benchmark)
i_const <- rep(mean(i_taylor), N_rules)

df_rules <- data.frame(
  date            = dates[1:N_rules],
  Pure.Taylor     = pmax(i_taylor, 0.03),
  Smoothed.Taylor = pmax(i_smooth, 0.03),
  Constant.Rate   = i_const
) %>%
  pivot_longer(
    cols      = c(Pure.Taylor, Smoothed.Taylor, Constant.Rate),
    names_to  = "Rule",
    values_to = "i"
  ) %>%
  mutate(Rule = recode(Rule,
    Pure.Taylor     = "Pure Taylor",
    Smoothed.Taylor = "Smoothed Taylor",
    Constant.Rate   = "Constant Rate"
  ))

ggplot(df_rules, aes(x = date, y = i * 100, color = Rule)) +
  geom_line(linewidth = 1.05) +
  scale_color_manual(values = c("#0f3460", "#e94560", "#888")) +
  labs(title    = "Policy rate path under different monetary rules",
       subtitle = "Small-Scale Model — illustrative simulation",
       x        = NULL, y = "Policy Rate (% p.a.)",
       caption  = "Parameters: α_π = 1.5 · α_y = 0.5 · λ = 0.7 (smoothing)") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  theme_mpp +
  theme(legend.title = element_blank())
Comparison of monetary policy rules under simulated shocks

Comparison of monetary policy rules under simulated shocks


7 Full System Simulation

7.1 Inflation Fan Chart

The MPP generates probability distributions for inflation via stochastic Monte Carlo simulation, allowing the construction of the fan chart published in the Inflation Report.

set.seed(1234)

T_proj  <- 12    # quarters ahead
N_sims  <- 2000  # Monte Carlo simulations

# System parameters
a1_pc    <- 0.45; a2_pc <- 0.20; a_fwd_pc <- 0.25
a3_pc    <- 0.25; a4_pc <- 0.12
b1_is    <- 0.70; b2_is <- -0.15; b3_is <- -0.25
lambda_r <- 0.65; phi_pi <- 1.5;  phi_y  <- 0.5

# Initial conditions
pi0 <- 0.055; pi_1 <- 0.060; y0 <- -0.01; i0 <- 0.115; e0 <- log(5.0)

simulate_mpp <- function(seed_s) {
  set.seed(seed_s)
  pi_v <- c(pi_1, pi0, numeric(T_proj))
  y_v  <- c(0,    y0,  numeric(T_proj))
  e_v  <- c(e0,   e0,  numeric(T_proj))
  i_v  <- c(i0,   i0,  numeric(T_proj))

  for (t in 3:(T_proj + 2)) {
    r_real   <- i_v[t-1] - pi_v[t-1]
    y_v[t]   <- b1_is * y_v[t-1] + b2_is * y_v[t-2] + b3_is * r_real + rnorm(1, 0, 0.007)
    pi_fwd_t <- pi_v[t-1] + rnorm(1, 0, 0.004)
    # FX change: computed from lagged values + contemporaneous shock
    fx_shock  <- rnorm(1, 0, 0.015)
    delta_e_t <- (i_v[t-1] - 0.055 - 0.03) / 4 + fx_shock
    pi_v[t]  <- a1_pc * pi_v[t-1] + a2_pc * pi_v[t-2] +
                a_fwd_pc * pi_fwd_t + a3_pc * y_v[t-1] +
                a4_pc * delta_e_t + rnorm(1, 0, 0.006)
    pi_v[t]  <- max(pi_v[t], 0)
    i_v[t]   <- (1 - lambda_r) * i_v[t-1] +
                lambda_r * (phi_pi * (pi_v[t] - pi_star) + phi_y * y_v[t] + 0.10)
    e_v[t]   <- e_v[t-1] + delta_e_t + rnorm(1, 0, 0.01)
  }
  pi_v[3:(T_proj + 2)]
}

mat_pi <- sapply(1:N_sims, simulate_mpp)

# Quantiles for the fan chart
qtis <- t(apply(mat_pi, 1, quantile,
                probs = c(0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90)))
df_fan <- as.data.frame(qtis)
colnames(df_fan) <- paste0("q", c(10, 20, 30, 40, 50, 60, 70, 80, 90))
df_fan$quarter <- seq(as.Date("2024-04-01"), by = "quarter", length.out = T_proj)

ggplot(df_fan, aes(x = quarter)) +
  geom_ribbon(aes(ymin = q10 * 100, ymax = q90 * 100), fill = "#0f3460", alpha = 0.12) +
  geom_ribbon(aes(ymin = q20 * 100, ymax = q80 * 100), fill = "#0f3460", alpha = 0.18) +
  geom_ribbon(aes(ymin = q30 * 100, ymax = q70 * 100), fill = "#0f3460", alpha = 0.22) +
  geom_ribbon(aes(ymin = q40 * 100, ymax = q60 * 100), fill = "#0f3460", alpha = 0.28) +
  geom_line(aes(y = q50 * 100), color = "#0f3460", linewidth = 1.3) +
  geom_hline(yintercept = pi_star * 100, linetype = "dashed",
             color = "#e94560", linewidth = 0.9) +
  annotate("text", x = df_fan$quarter[2], y = pi_star * 100 + 0.3,
           label = paste0("Target: ", pi_star * 100, "%"),
           color = "#e94560", size = 3.8, fontface = "bold") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_x_date(date_labels = "%Y", date_breaks = "1 year") +
  labs(
    title    = "Fan Chart — Inflation Projection (CPI)",
    subtitle = paste0("Monte Carlo distribution (N = ", N_sims,
                      " simulations) — 20/40/60/80% confidence bands"),
    x        = NULL, y = "CPI (% p.a.)",
    caption  = "Small-Scale Model (MPP) — simulated data for pedagogical purposes"
  ) +
  theme_mpp
Inflation Fan Chart — stochastic simulation

Inflation Fan Chart — stochastic simulation


8 Policy Shock Simulation

8.1 Contractionary SELIC Shock

We simulate a +200 bps shock to the policy rate and analyze the response of endogenous variables over time (stylized impulse response function):

simulate_shock <- function(delta_selic = 0.02, seed_s = 42) {
  set.seed(seed_s)
  T_sim <- 16
  pi_v  <- c(0.060, 0.055, numeric(T_sim))
  y_v   <- c(0.005, -0.01, numeric(T_sim))
  i_v   <- c(0.115, 0.115 + delta_selic, numeric(T_sim))

  for (t in 3:(T_sim + 2)) {
    r_real  <- i_v[t-1] - pi_v[t-1]
    y_v[t]  <- b1_is * y_v[t-1] + b2_is * y_v[t-2] + b3_is * r_real + rnorm(1, 0, 0.004)
    pi_fwd  <- pi_v[t-1] + rnorm(1, 0, 0.003)
    pi_v[t] <- a1_pc * pi_v[t-1] + a2_pc * pi_v[t-2] +
               a_fwd_pc * pi_fwd + a3_pc * y_v[t-1] + rnorm(1, 0, 0.004)
    pi_v[t] <- max(pi_v[t], 0)
    i_v[t]  <- i_v[2]   # policy rate held at new level
  }
  data.frame(
    t         = 1:T_sim,
    inflation = pi_v[3:(T_sim+2)] * 100,
    gap       = y_v[3:(T_sim+2)] * 100,
    selic     = i_v[3:(T_sim+2)] * 100,
    scenario  = ifelse(delta_selic > 0, "Shock +200bps", "Baseline")
  )
}

df_base  <- simulate_shock(0.00)
df_shock <- simulate_shock(0.02)

df_irf <- bind_rows(df_base, df_shock)

colors_irf <- c("Baseline" = "#888", "Shock +200bps" = "#e94560")

p_irf_pi <- ggplot(df_irf, aes(x=t, y=inflation, color=scenario)) +
  geom_hline(yintercept=pi_star*100, linetype="dotted", color="#0f3460") +
  geom_line(linewidth=1.1) +
  scale_color_manual(values=colors_irf, name="") +
  scale_x_continuous(breaks=seq(2,16,2)) +
  labs(title="Inflation (% p.a.)", x="Quarters", y=NULL) +
  theme_mpp + theme(legend.position="none")

p_irf_y <- ggplot(df_irf, aes(x=t, y=gap, color=scenario)) +
  geom_hline(yintercept=0, linetype="dotted", color="#888") +
  geom_line(linewidth=1.1) +
  scale_color_manual(values=colors_irf, name="") +
  scale_x_continuous(breaks=seq(2,16,2)) +
  labs(title="Output gap (%)", x="Quarters", y=NULL) +
  theme_mpp + theme(legend.position="none")

p_irf_i <- ggplot(df_irf, aes(x=t, y=selic, color=scenario)) +
  geom_line(linewidth=1.1) +
  scale_color_manual(values=colors_irf, name="") +
  scale_x_continuous(breaks=seq(2,16,2)) +
  labs(title="Policy Rate (% p.a.)", x="Quarters", y=NULL) +
  theme_mpp

(p_irf_pi | p_irf_y) / p_irf_i +
  plot_annotation(
    title    = "Impulse Response Function — Contractionary Shock",
    subtitle = "Shock of +200 bps to the policy rate | MPP — hybrid specification",
    caption  = "Reading: disinflation operates primarily via the demand channel (output gap) with a 2–3 quarter lag."
  )
Impulse response: +200 bps policy rate shock

Impulse response: +200 bps policy rate shock


9 Limitations and Extensions

9.1 Main Limitations of the Original MPP

MPP limitations and extensions in the literature
Limitation Description Natural Extension
Linearity Non-linear dynamics (e.g., ZLB, exchange rate regimes) are not captured DSGE models with nominal and real frictions (SAMBA, BCB)
Partially rational expectations The hybrid specification is an ad hoc solution to expectations formation Survey expectations (Focus/BCB) as an exogenous proxy
Simplified external sector Capital account and trade flows are not modeled explicitly Two-country model with trade and capital account (NOEM)
No financial sector Credit channel, balance sheets, and domestic risk premiums are absent DSGE with financial intermediaries (Bernanke, Gertler, Gilchrist)
Vulnerability to the Lucas critique Parameters may shift with the policy regime — risk in the backward-looking specification Regime-switching models (MS-VAR, MS-DSGE)

9.2 The Road to SAMBA

The SAMBA model (Stochastic Analytical Model with a Bayesian Approach), developed by the BCB from 2007 onward, is the natural evolution of the MPP:

  • Full micro-foundations (households, firms, central bank)
  • Bayesian estimation of structural parameters
  • Explicit external sector with a modeled trading partner
  • Nominal and real rigidities (Calvo pricing, capital adjustment costs)
  • Fan charts with identified shocks via the structural covariance matrix

10 Conclusion

The Small-Scale Model was the backbone of COPOM decisions in the early years of Brazil’s inflation-targeting regime. Its strength lies in clarity:

  1. IS Curve — captures monetary policy effects on demand with a 1–2 quarter lag
  2. Phillips Curve — translates demand pressures and exchange rate shocks into inflation
  3. UIP — links the exchange rate to the interest rate differential and country risk
  4. Policy rule — closes the model with a central bank reaction function

The methodological lesson remains relevant today: simple, well-communicated models have irreplaceable institutional value, even when more sophisticated tools are available.


11 References

  • Bogdanski, J., Tombini, A.A. & Werlang, S.R.C. (2000). Implementing Inflation Targeting in Brazil. Working Paper 1, Banco Central do Brasil.
  • Goldfajn, I. & Werlang, S.R.C. (1999). The Pass-through from Depreciation to Inflation. Mimeo, PUC-Rio / BCB.
  • Taylor, J.B. (1993). Discretion versus Policy Rules in Practice. Carnegie-Rochester Conference Series on Public Policy, 39, 195–214.
  • Svensson, L.E.O. (1997). Inflation Forecast Targeting: Implementing and Monitoring Inflation Targets. European Economic Review, 41(6).
  • Hodrick, R.J. & Prescott, E.C. (1997). Postwar U.S. Business Cycles. Journal of Money, Credit and Banking, 29(1), 1–16.
  • Castro, M.R. et al. (2011). SAMBA: Stochastic Analytical Model with a Bayesian Approach. Working Paper 239, BCB.

Note: all data used in this document are simulated for pedagogical purposes, except where explicitly stated.