Methodological positioning — read before proceeding.
This document does not replicate the operational model used by the Central Bank of Brazil in 1999. It develops a modern, monthly, semi-structural adaptation inspired by the four-block architecture of Bogdanski, Tombini & Werlang (2000): aggregate demand (IS), inflation dynamics (Phillips), exchange-rate transmission (UIP), and policy rules (Taylor).
Key differences from the original MPP:
Use this note as a pedagogical reference for the monetary policy transmission mechanism in Brazil, not as a replication of the BCB’s internal toolkit.
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.”
The MPP occupies a strategic position in the model hierarchy: small enough to explain to a policy committee, structured enough to simulate alternative monetary policy scenarios.
The MPP consists of four main blocks. The diagram below shows their articulation (static — hover on subsequent charts for detail):
\[\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\) is the ex-ante real interest rate.
Neutral rate note. The original MPP imposed \(r^* = g^*\) as a hard structural restriction. This adaptation estimates a statistical trend of the ex-ante real rate as a proxy. That average reflects BCB reactions to shocks and crises — it is not automatically the equilibrium rate consistent with potential growth.
set.seed(42)
n <- 120
trend <- cumsum(c(0.005, rep(0.006, n-1) + rnorm(n-1, 0, 0.001)))
cycle_v <- numeric(n)
for(t in 2:n) cycle_v[t] <- 0.65 * cycle_v[t-1] + rnorm(1, 0, 0.01)
idx_gdp <- 100 + trend + cycle_v
dates <- seq(as.Date("1995-01-01"), by = "quarter", length.out = n)
df_gdp <- data.frame(date = dates, idx_gdp = idx_gdp)
hp_result <- mFilter::hpfilter(df_gdp$idx_gdp, freq = 1600)
df_gdp$potential <- hp_result$trend
df_gdp$gap <- hp_result$cycle
# ── Chart 1: observed vs potential ─────────────────────────────────────────────
p_gdp <- plot_ly(df_gdp, x = ~date) %>%
add_lines(y = ~idx_gdp,
name = "Observed GDP",
line = list(color = clr_main, width = 2),
hovertemplate = "%{x|%Y-%m}<br>Observed: %{y:.2f}<extra></extra>") %>%
add_lines(y = ~potential,
name = "Potential GDP (HP)",
line = list(color = clr_red, width = 2, dash = "dash"),
hovertemplate = "%{x|%Y-%m}<br>Potential: %{y:.2f}<extra></extra>") %>%
layout(
title = list(text = "<b>Observed GDP vs. Potential Output</b><br><sub>Hodrick-Prescott filter (λ = 1,600) — simulated data</sub>",
font = list(size = 13, color = clr_main)),
xaxis = list(title = ""),
yaxis = list(title = "Index (simulated)"),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
# ── Chart 2: output gap ─────────────────────────────────────────────────────────
p_gap <- plot_ly(df_gdp, x = ~date) %>%
add_ribbons(ymin = ~pmin(gap, 0), ymax = ~pmax(gap, 0),
fillcolor = paste0(clr_main, "30"),
line = list(color = "transparent"),
name = "Gap area", showlegend = FALSE,
hoverinfo = "skip") %>%
add_lines(y = ~gap,
name = "Output gap",
line = list(color = clr_main, width = 2),
hovertemplate = "%{x|%Y-%m}<br>Gap: %{y:.4f}<extra></extra>") %>%
layout(shapes = list(hline(0, clr_grey, "dot"))) %>%
layout(
title = list(text = "<b>Output Gap</b><br><sub>Deviation from HP trend (index units)</sub>",
font = list(size = 13, color = clr_main)),
xaxis = list(title = ""),
yaxis = list(title = "Gap (index units)"),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
subplot(p_gdp, p_gap, nrows = 2, shareX = TRUE, titleY = TRUE) %>%
layout(hovermode = "x unified") %>%
config(plt_cfg)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)
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) %>%
mutate(term = c("β₀ (intercept)","β₁ (ỹ_{t-1})","β₂ (ỹ_{t-2})","β₃ (r_{t-1})"),
across(where(is.numeric), ~round(.x,4))) %>%
rename(Parameter=term, Estimate=estimate,
`Std. Error`=std.error, `t-stat`=statistic, `p-value`=p.value),
caption = "**IS Curve — OLS with HAC errors (Newey-West, 4 lags)**"
) %>%
kableExtra::kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE)| 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 <- plot_ly(df_is, x = ~date) %>%
add_lines(y = ~y*100, name = "Observed",
line = list(color = clr_main, width = 1.8),
hovertemplate = "%{x|%Y-%m}<br>Obs: %{y:.3f}<extra></extra>") %>%
add_lines(y = ~fitted*100, name = "Fitted",
line = list(color = clr_red, width = 1.8, dash = "dash"),
hovertemplate = "%{x|%Y-%m}<br>Fitted: %{y:.3f}<extra></extra>") %>%
layout(title = list(text = "<b>Observed vs. fitted output gap</b>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = ""), yaxis = list(title = "Gap × 100"),
hovermode = "x unified", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel)
p_res <- plot_ly(df_is, x = ~date) %>%
add_bars(y = ~resid*100, name = "Residual",
marker = list(color = paste0(clr_blue, "99")),
hovertemplate = "%{x|%Y-%m}<br>Residual: %{y:.4f}<extra></extra>") %>%
layout(shapes = list(hline(0, clr_grey, "dot"))) %>%
layout(title = list(text = "<b>IS Curve residuals</b>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = ""), yaxis = list(title = "Residual"),
hovermode = "x unified", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel)
subplot(p_fit, p_res, nrows = 2, shareX = TRUE, titleY = TRUE) %>%
layout(hovermode = "x unified") %>%
config(plt_cfg)\[\boxed{\pi_t = \underbrace{\alpha_1 \pi_{t-1} + \alpha_2 \pi_{t-2}}_{\text{inertia}} + \underbrace{\alpha_{fwd}\,\mathbb{E}_t[\pi_{t+1}]}_{\text{expectations}} + \alpha_3 \tilde{y}_{t-1} + \alpha_4 \Delta \ln E_t + \varepsilon_t}\]
Long-run restriction: \(\alpha_1 + \alpha_2 + \alpha_{fwd} = 1\).
OLS caveat. This adaptation does not formally impose the long-run restriction. Additionally, 12-month accumulated CPI creates mechanical autocorrelation across observations; and Focus expectations should be verified as genuinely ex-ante to avoid look-ahead bias.
set.seed(21)
# α₁ + α₂ + α_fwd = 0.45 + 0.20 + 0.35 = 1.00 ✓ (long-run neutrality)
# α₃ (output gap) and α₄ (FX) are separate — not part of the inflation restriction
alpha1 <- 0.45; alpha2 <- 0.20; alpha_fwd <- 0.35
alpha3 <- 0.25; alpha4 <- 0.12
pi_star <- 0.045
delta_e <- rnorm(n, 0, 0.04)
eps_pc <- rnorm(n, 0, 0.008)
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)
# ── Panel 1: simulated inflation ────────────────────────────────────────────────
p_pi <- plot_ly(df_pc, x = ~date) %>%
add_lines(y = ~inflation*100, name = "Inflation",
line = list(color = clr_red, width = 2),
hovertemplate = "%{x|%Y-%m}<br>Inflation: %{y:.2f}%<extra></extra>") %>%
layout(shapes = list(hline(pi_star*100, clr_main, "dash", 1.5))) %>%
layout(
title = list(text = "<b>Simulated inflation — Hybrid Phillips Curve</b><br><sub>α₁ + α₂ + α_fwd = 1.00 (long-run neutrality)</sub>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = ""), yaxis = list(title = "CPI (% y/y)", ticksuffix = "%"),
hovermode = "x unified", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
# ── Panel 2: pass-through specifications ───────────────────────────────────────
E_seq <- seq(1.5, 6.5, length.out = 100)
df_pt <- data.frame(
E = E_seq,
Constant = 0.12,
Quadratic = 0.08 + 0.04 * (log(E_seq))^2,
Level = pmax(0.05, 0.22 - 0.03 * log(E_seq))
)
p_pt <- plot_ly(df_pt, x = ~E) %>%
add_lines(y = ~Constant, name = "Constant",
line = list(color = clr_main, width = 2),
hovertemplate = "BRL/USD: %{x:.2f}<br>α₄ (const): %{y:.3f}<extra></extra>") %>%
add_lines(y = ~Quadratic, name = "Quadratic in Δe",
line = list(color = clr_red, width = 2),
hovertemplate = "BRL/USD: %{x:.2f}<br>α₄ (quad): %{y:.3f}<extra></extra>") %>%
add_lines(y = ~Level, name = "Level-dependent",
line = list(color = clr_teal, width = 2),
hovertemplate = "BRL/USD: %{x:.2f}<br>α₄ (level): %{y:.3f}<extra></extra>") %>%
layout(
title = list(text = "<b>Exchange rate pass-through: alternative specifications</b><br><sub>Goldfajn & Werlang (1999)</sub>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = "Exchange Rate (BRL/USD)"),
yaxis = list(title = "Coefficient α₄"),
hovermode = "x", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
subplot(p_pi, p_pt, nrows = 2, shareX = FALSE, titleY = TRUE) %>%
config(plt_cfg)With \(e_t = \ln(\text{BRL/USD})\), the UIP in first differences is:
\[\boxed{\Delta e_t = -\alpha_i \Delta i_t + \alpha_f \Delta i^*_t + \alpha_x \Delta x_t + \eta_t}\]
with \(\alpha_i > 0,\ \alpha_f > 0,\ \alpha_x > 0\).
Sign error to avoid. The domestic SELIC rate must enter with a negative coefficient. Under \(e_t = \ln(\text{BRL/USD})\), a COPOM hike appreciates the BRL (reduces \(e_t\)). Writing it with a positive sign inverts the entire exchange-rate transmission channel.
set.seed(99)
i_ext <- 0.055
x_risk <- numeric(n); e_log <- numeric(n)
x_risk[1] <- 300; e_log[1] <- log(3.5)
for(t in 2:n) {
x_risk[t] <- max(0.7*x_risk[t-1] + rnorm(1,0,25) - 15*(t>60), 50)
selic_t <- 0.12 - 0.02*(t/n) + rnorm(1,0,0.015)
# UIP: higher domestic rate → BRL appreciation → negative sign
e_log[t] <- e_log[t-1] - (selic_t - i_ext - x_risk[t]/10000)/4 + rnorm(1,0,0.025)
}
df_uip <- data.frame(date = dates, fx = exp(e_log), risk = x_risk)
p_e <- plot_ly(df_uip, x = ~date) %>%
add_lines(y = ~fx, name = "BRL/USD",
line = list(color = clr_main, width = 2),
hovertemplate = "%{x|%Y-%m}<br>BRL/USD: %{y:.3f}<extra></extra>") %>%
layout(
title = list(text = "<b>Simulated exchange rate (BRL/USD)</b><br><sub>Higher domestic rate → BRL appreciation (correct UIP sign)</sub>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = ""), yaxis = list(title = "BRL/USD"),
hovermode = "x unified", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
p_x <- plot_ly(df_uip, x = ~date) %>%
add_lines(y = ~risk, name = "EMBI+",
line = list(color = clr_red, width = 2),
hovertemplate = "%{x|%Y-%m}<br>EMBI+: %{y:.0f} bps<extra></extra>") %>%
layout(shapes = list(hline(200, clr_grey, "dot"))) %>%
layout(
title = list(text = "<b>Country risk premium (EMBI+)</b><br><sub>AR(1) with gradual fiscal improvement</sub>",
font = list(size = 12, color = clr_main)),
xaxis = list(title = ""), yaxis = list(title = "Basis Points (bps)"),
hovermode = "x unified", plot_bgcolor="white", paper_bgcolor="white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
)
subplot(p_e, p_x, nrows = 2, shareX = TRUE, titleY = TRUE) %>%
layout(hovermode = "x unified") %>%
config(plt_cfg)\[\boxed{i_t = (1-\lambda)\,i_{t-1} + \lambda \left[\alpha_1(\pi_t - \pi^*) + \alpha_2 \tilde{y}_t + \alpha_3\right]}\]
Normative vs. positive. The original MPP used the rule normatively — to find the SELIC path consistent with target convergence under scenario X. Using it as a positive predictor of COPOM decisions is a different exercise. Label rule-implied paths as “policy-consistent path under the estimated reaction function”, not as SELIC forecasts.
set.seed(55)
N_rules <- 80
alpha_pi_r <- 1.5; alpha_y_r <- 0.5; lambda_sm <- 0.7
i_taylor <- alpha_pi_r*(pi[1:N_rules]-pi_star) + alpha_y_r*df_gdp$gap[1:N_rules] + 0.10
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]
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 = pmax(i_smooth, 0.03),
Constant = i_const)
plot_ly(df_rules, x = ~date) %>%
add_lines(y = ~Pure.Taylor*100, name = "Pure Taylor",
line = list(color = clr_main, width = 2),
hovertemplate = "%{x|%Y-%m}<br>Pure Taylor: %{y:.1f}%<extra></extra>") %>%
add_lines(y = ~Smoothed*100, name = "Smoothed Taylor (λ=0.7)",
line = list(color = clr_red, width = 2),
hovertemplate = "%{x|%Y-%m}<br>Smoothed: %{y:.1f}%<extra></extra>") %>%
add_lines(y = ~Constant*100, name = "Constant Rate",
line = list(color = clr_grey, width = 2, dash = "dash"),
hovertemplate = "%{x|%Y-%m}<br>Constant: %{y:.1f}%<extra></extra>") %>%
layout(
title = list(
text = "<b>Policy rate path under alternative rules</b><br><sub>Illustrative simulation — α_π = 1.5, α_y = 0.5</sub>",
font = list(size = 13, color = clr_main)
),
xaxis = list(title = ""),
yaxis = list(title = "Policy Rate (% p.a.)", ticksuffix = "%"),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white",
legend = plt_layout$legend, font = plt_layout$font,
margin = plt_layout$margin, hoverlabel = plt_layout$hoverlabel
) %>%
config(plt_cfg)set.seed(1234)
T_proj <- 12; N_sims <- 2000
a1_pc <- 0.45; a2_pc <- 0.20; a_fwd_pc <- 0.35 # sum = 1.00 ✓
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
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))
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)
delta_e_t <- -(i_v[t-1]-0.055-0.03)/4 + rnorm(1,0,0.015)
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)
}
pi_v[3:(T_proj+2)]
}
mat_pi <- sapply(1:N_sims, simulate_mpp)
qtis <- as.data.frame(t(apply(mat_pi, 1, quantile,
probs = c(.10,.20,.30,.40,.50,.60,.70,.80,.90))))
colnames(qtis) <- paste0("q", c(10,20,30,40,50,60,70,80,90))
qtis$quarter <- seq(as.Date("2024-04-01"), by="quarter", length.out=T_proj)
# ── Full plotly fan chart with interactive bands ────────────────────────────────
plot_ly(qtis, x = ~quarter) %>%
# 80% band (p10–p90)
add_ribbons(ymin = ~q10*100, ymax = ~q90*100,
fillcolor = paste0(clr_main, "1A"),
line = list(color = "transparent"),
name = "80% band", legendgroup = "bands",
hoverinfo = "skip") %>%
# 60% band (p20–p80)
add_ribbons(ymin = ~q20*100, ymax = ~q80*100,
fillcolor = paste0(clr_main, "2A"),
line = list(color = "transparent"),
name = "60% band", legendgroup = "bands",
hoverinfo = "skip") %>%
# 40% band (p30–p70)
add_ribbons(ymin = ~q30*100, ymax = ~q70*100,
fillcolor = paste0(clr_main, "3A"),
line = list(color = "transparent"),
name = "40% band", legendgroup = "bands",
hoverinfo = "skip") %>%
# 20% band (p40–p60)
add_ribbons(ymin = ~q40*100, ymax = ~q60*100,
fillcolor = paste0(clr_main, "45"),
line = list(color = "transparent"),
name = "20% band", legendgroup = "bands",
hoverinfo = "skip") %>%
# Median line with rich tooltip
add_lines(y = ~q50*100,
name = "Median (p50)",
line = list(color = clr_main, width = 3),
hovertemplate = paste0(
"<b>%{x|%Y-Q%q}</b><br>",
"Median: %{y:.2f}%<br>",
"<i>Hover bands for range</i><extra></extra>"
)) %>%
# p10 / p90 boundary lines (subtle)
add_lines(y = ~q10*100, name = "p10",
line = list(color = clr_main, width = 0.7, dash = "dot"),
showlegend = FALSE,
hovertemplate = "p10: %{y:.2f}%<extra></extra>") %>%
add_lines(y = ~q90*100, name = "p90",
line = list(color = clr_main, width = 0.7, dash = "dot"),
showlegend = FALSE,
hovertemplate = "p90: %{y:.2f}%<extra></extra>") %>%
# Inflation target
add_lines(y = rep(pi_star*100, T_proj),
x = ~quarter,
name = paste0("Target (", pi_star*100, "%)"),
line = list(color = clr_red, width = 1.8, dash = "dash"),
hovertemplate = "Target: %{y:.1f}%<extra></extra>") %>%
layout(
title = list(
text = paste0("<b>Fan Chart — CPI Inflation Projection</b><br>",
"<sub>Monte Carlo (N=", N_sims, " paths) — 20/40/60/80% conditional forecast bands</sub>"),
font = list(size = 14, color = clr_main)
),
xaxis = list(title = "", tickformat = "%Y"),
yaxis = list(title = "CPI (% p.a.)", ticksuffix = "%"),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white",
legend = list(orientation = "h", y = -0.18, font = list(size = 11)),
font = plt_layout$font,
margin = list(l=60, r=30, t=80, b=60),
hoverlabel = plt_layout$hoverlabel
) %>%
config(plt_cfg)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]
}
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, "+200bps shock", "Baseline"))
}
df_irf <- bind_rows(simulate_shock(0.00), simulate_shock(0.02))
make_irf_line <- function(y_var, label_var, y_title, add_target = FALSE) {
df_base <- df_irf %>% filter(scenario == "Baseline")
df_shock <- df_irf %>% filter(scenario == "+200bps shock")
p <- plot_ly() %>%
add_lines(data = df_base,
x = ~t, y = ~get(y_var),
name = "Baseline",
line = list(color = clr_grey, width = 2, dash = "dot"),
hovertemplate = paste0("Q%{x}<br>Baseline ", label_var, ": %{y:.2f}%<extra></extra>")) %>%
add_lines(data = df_shock,
x = ~t, y = ~get(y_var),
name = "+200bps",
line = list(color = clr_red, width = 2.5),
hovertemplate = paste0("Q%{x}<br>+200bps ", label_var, ": %{y:.2f}%<extra></extra>"))
if (add_target)
p <- p %>% layout(shapes = list(hline(pi_star*100, clr_main, "dot", 1)))
p %>% layout(
title = list(text = paste0("<b>", y_title, "</b>"),
font = list(size = 12, color = clr_main)),
xaxis = list(title = "Quarters", dtick = 2),
yaxis = list(title = paste0(y_title, " (%)"), ticksuffix = "%"),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white",
legend = plt_layout$legend, font = plt_layout$font, hoverlabel = plt_layout$hoverlabel
)
}
p_inf <- make_irf_line("inflation", "Inflation", "Inflation", add_target = TRUE)
p_gap2 <- make_irf_line("gap", "Gap", "Output gap", add_target = FALSE)
p_sel <- make_irf_line("selic", "Selic", "Policy rate", add_target = FALSE)
subplot(
subplot(p_inf, p_gap2, shareX = TRUE, titleY = TRUE),
p_sel,
nrows = 2, shareX = TRUE, titleY = TRUE
) %>%
layout(
title = list(
text = "<b>Impulse Response — Contractionary Policy Shock (+200 bps)</b><br><sub>Disinflation via demand channel with ~2–3 quarter lag</sub>",
font = list(size = 13, color = clr_main)
),
hovermode = "x unified",
plot_bgcolor = "white", paper_bgcolor = "white"
) %>%
config(plt_cfg)| Issue | Description | Recommended Fix |
|---|---|---|
| Long-run restriction not imposed (Phillips) | OLS does not enforce α₁+α₂+α_fwd=1; holds in simulation but not estimation | Impose as linear constraint in nlm() or systemfit |
| Overlapping 12m inflation variable | 12m CPI creates mechanical autocorrelation; monthly or non-overlapping measures preferred | Use monthly or trimestral non-overlapping inflation |
| UIP not fully identified | Estimated as VAR/OLS auxiliary; foreign rate proxied rather than measured | Use a mapped foreign rate series; impose UIP sign restriction |
| Neutral rate is a statistical trend | r* estimated from rolling ex-ante real rates — reflects policy reactions, not structural equilibrium | Report r* scenarios: {4%, 5%, 6%} |
| OLS with no simultaneity correction | IS, Phillips, and UIP are simultaneously determined; OLS does not identify structural channels | Instrument with lagged values or use SVAR identification |
| Monthly frequency (vs. original quarterly) | IBC-Br is a reasonable proxy but introduces differences vs. quarterly GDP | Acknowledge frequency adaptation; verify lag structure in months |
All data are simulated for pedagogical purposes. This document is an adaptation — not a replication — of the BCB Small-Scale Model.