Impact of Oil Price Increases on China

Sign-Restricted BVAR, Impulse Response Analysis, and Structural Scenario Analysis

Show code
required_packages <- c(
  "readxl",
  "ggplot2",
  "dplyr",
  "tidyr",
  "knitr",
  "scales",
  "bsvarSIGNs",
  "bsvars"
)

for (pkg in required_packages) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    install.packages(pkg)
  }
}

if (!requireNamespace("APRScenario", quietly = TRUE)) {
  stop("APRScenario package is required for scenario analysis")
}

suppressPackageStartupMessages({
  library(readxl)
  library(ggplot2)
  library(dplyr)
  library(tidyr)
  library(knitr)
  library(scales)
  library(bsvarSIGNs)
  library(bsvars)
  library(APRScenario)
})

knitr::opts_chunk$set(
  fig.width = 11,
  fig.height = 6,
  dpi = 180,
  out.width = "100%",
  fig.align = "center",
  fig.path = "markdown-with-code_files/figure-html/",
  cache.path = "markdown-with-code_cache/",
  cache = TRUE
)

theme_set(
  theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(face = "bold", size = 15),
      plot.subtitle = element_text(size = 11, colour = "#4a5568"),
      strip.text = element_text(face = "bold"),
      panel.grid.minor = element_blank(),
      legend.position = "bottom"
    )
)

var_names <- c("oil", "ip", "cpi")
var_labels <- c(
  oil = "Oil price",
  ip = "Industrial production",
  cpi = "Core CPI"
)
shock_names <- c(
  "Oil supply shock",
  "Aggregate supply shock",
  "Aggregate demand shock"
)
scenario_labels <- c(
  "oil price stay at 100" = "Oil stays at 100 from Mar-Dec 2026",
  "oil price return to 80 in mid-2026" = "Oil returns to 80 from Jul 2026"
)
variable_colors <- c(
  "Oil price" = "#b75d0a",
  "Industrial production" = "#1f77b4",
  "Core CPI" = "#2f855a"
)
scenario_colors <- c(
  "Oil stays at 100 from Mar-Dec 2026" = "#c53030",
  "Oil returns to 80 from Jul 2026" = "#2b6cb0"
)
interval_probability <- 0.68
alpha_tail <- (1 - interval_probability) / 2
target_oil_impact <- 100 * log(1.10)

fmt_num <- function(x, digits = 2) {
  formatC(x, format = "f", digits = digits)
}

summarise_draws <- function(draw_matrix, horizon_values, alpha = alpha_tail) {
  data.frame(
    horizon = horizon_values,
    median = apply(draw_matrix, 1, median),
    lower = apply(draw_matrix, 1, quantile, probs = alpha),
    upper = apply(draw_matrix, 1, quantile, probs = 1 - alpha)
  )
}

1. Introduction

The primary objective of this report is to analyze how the recent surge in oil prices, driven by the ongoing conflict in the Middle East, could affect the Chinese economy. Specifically, we focus on how this conflict-driven supply shock affects China’s industrial production and core Consumer Price Index (CPI). To provide a comprehensive assessment, the report first quantifies the baseline effects of a standardized 10% increase in oil prices. It then evaluates broader projections under different future trajectories, such as a scenario in which oil remains at $100 through the end of 2026 and another in which it moderates to $80 from July 2026 onward.

To conduct this analysis, we employ a three-variable Bayesian Vector Autoregression (BVAR) model. The model is estimated in transformed levels using 100 * log(level) for oil prices, industrial production, and core CPI. To ensure robust estimation, the prior structure combines a Minnesota prior with sum-of-coefficients and single-unit-root components. In addition, sign restrictions are applied to identify the underlying structural shocks more precisely, because the same increase in oil prices can have very different macroeconomic implications depending on whether it is driven by supply disruptions, stronger demand, or other aggregate forces.

To interpret the model’s outputs, the report relies on two complementary analytical tools:

Impulse Response Functions (IRFs): IRFs are used to isolate and measure the dynamic, period-by-period effect of a single, one-off shock. In this report, the IRFs are rescaled so that each structural shock corresponds exactly to a 10% oil-price increase on impact. This allows us to observe the pure, isolated reaction of the Chinese economy to a sudden, immediate price spike, holding other factors constant.

Structural Scenario Analysis (Antolin-Diaz et al., 2021): In contrast to IRFs, Structural Scenario Analysis (implemented via APRScenario) is used to evaluate sustained, hypothetical future paths. Rather than focusing on an isolated shock, this method imposes a continuous future trajectory for oil prices. Under an oil-supply interpretation, it traces the macroeconomic consequences for China under specific narratives (e.g., persistently high prices caused by prolonged conflict versus a steady de-escalation in prices).

2. Data

Show code
data_log_sa <- read_excel("data_log_sa.xlsx", sheet = "data_log_sa") |>
  transmute(
    date = as.Date(t),
    oil = oil,
    ip = ip,
    cpi = cpi
  )

sample_start <- min(data_log_sa$date)
sample_end <- max(data_log_sa$date)

data_dictionary <- data.frame(
  Variable = c("Oil price", "Industrial production", "Core CPI"),
  Code = c("oil", "ip", "cpi"),
  Role = c(
    "Observed oil-price series used for structural identification and scenario conditioning",
    "Monthly indicator of real activity in China",
    "Monthly indicator of underlying consumer inflation in China"
  ),
  `Transformation used in estimation` = rep("100 * log(level)", 3),
  check.names = FALSE
)

data_plot_df <- data_log_sa |>
  pivot_longer(cols = all_of(var_names), names_to = "variable", values_to = "value") |>
  mutate(variable = factor(unname(var_labels[variable]), levels = unname(var_labels[var_names])))

The model uses monthly data from 一月 2005 to 二月 2026. The variables are entered in transformed levels as 100 * log(level). This keeps the analysis in levels, which is important for the non-stationary BVAR specification, while still allowing changes to be interpreted in approximate percentage terms.

Show code
kable(data_dictionary, align = c("l", "l", "l", "l"))
Variable Code Role Transformation used in estimation
Oil price oil Observed oil-price series used for structural identification and scenario conditioning 100 * log(level)
Industrial production ip Monthly indicator of real activity in China 100 * log(level)
Core CPI cpi Monthly indicator of underlying consumer inflation in China 100 * log(level)
Show code
ggplot(data_plot_df, aes(x = date, y = value, colour = variable)) +
  geom_line(linewidth = 0.8, show.legend = FALSE) +
  facet_wrap(~ variable, ncol = 1, scales = "free_y") +
  scale_colour_manual(values = variable_colors) +
  scale_x_date(date_breaks = "4 years", date_labels = "%Y") +
  labs(
    title = "Series used in estimation",
    subtitle = "Monthly transformed levels for oil, industrial production, and core CPI",
    x = NULL,
    y = "100 * log(level)"
  )

Data used in the BVAR. All three series are plotted in the transformed units used for estimation: 100 * log(level).

3. Model specification and settings

Show code
model_data <- as.matrix(data_log_sa[, var_names])
storage.mode(model_data) <- "double"
colnames(model_data) <- var_names

if (anyNA(model_data)) {
  stop("model_data contains NA values")
}

p <- 12L
irf_horizon <- 24L
hyper_draws <- 20000L
hyper_burn_in <- 5000L
posterior_draws <- 10000L

sign_irf <- matrix(NA_integer_, nrow = length(var_names), ncol = length(var_names))
rownames(sign_irf) <- var_names
colnames(sign_irf) <- shock_names

sign_irf[, 1] <- c(1L, -1L, 1L)
sign_irf[, 2] <- c(NA_integer_, 1L, -1L)
sign_irf[, 3] <- c(1L, 1L, 1L)

spec <- specify_bsvarSIGN$new(
  data = model_data,
  p = p,
  sign_irf = sign_irf,
  max_tries = 100000L,
  stationary = c(TRUE,FALSE, FALSE)
)

spec$prior$hyper[1, ] <- 12.5
spec$prior$hyper[2, ] <- 2.5
spec$prior$hyper[3, ] <- 0.1

model_settings <- data.frame(
  Setting = c(
    "Sample",
    "Frequency",
    "Variables",
    "Lag length",
    "Prior structure",
    "Fixed hyperparameters",
    "Posterior draws",
    "IRF horizon",
    "Credible interval",
    "Scenario horizon",
    "Structural scenario driver"
  ),
  Value = c(
    paste(format(sample_start, "%b %Y"), "to", format(sample_end, "%b %Y")),
    "Monthly",
    "Oil price, industrial production, core CPI",
    "12 lags",
    "Minnesota prior, sum-of-coefficients prior, and single-unit-root prior",
    "0.1, 12.5, 2.5",
    "10,000",
    "24 months",
    "68% posterior interval",
    "10 months",
    "Oil supply shock only; shocks 2 and 3 are fixed to zero in APRScenario"
  ),
  check.names = FALSE
)

sign_restrictions <- data.frame(
  Shock = shock_names,
  `Oil price` = c("Positive", "Unrestricted", "Positive"),
  `Industrial production` = c("Negative", "Positive", "Positive"),
  `Core CPI` = c("Positive", "Negative", "Positive"),
  check.names = FALSE
)
Show code
kable(model_settings, align = c("l", "l"))
Setting Value
Sample 1月 2005 to 2月 2026
Frequency Monthly
Variables Oil price, industrial production, core CPI
Lag length 12 lags
Prior structure Minnesota prior, sum-of-coefficients prior, and single-unit-root prior
Fixed hyperparameters 0.1, 12.5, 2.5
Posterior draws 10,000
IRF horizon 24 months
Credible interval 68% posterior interval
Scenario horizon 10 months
Structural scenario driver Oil supply shock only; shocks 2 and 3 are fixed to zero in APRScenario

Structural identification is based on impact sign restrictions. The restrictions are summarized below.

Show code
kable(sign_restrictions, align = c("l", "l", "l", "l"))
Shock Oil price Industrial production Core CPI
Oil supply shock Positive Negative Positive
Aggregate supply shock Unrestricted Positive Negative
Aggregate demand shock Positive Positive Positive
Show code
set.seed(123)
post <- estimate(spec, S = posterior_draws, thin = 1L, show_progress = TRUE)

hyper_draws_matrix <- t(spec$prior$hyper)
colnames(hyper_draws_matrix) <- paste0("hyper_", seq_len(ncol(hyper_draws_matrix)))

post_is_normalised <- post$is_normalised()
skipped_draws <- post$posterior$skipped

These restrictions imply the following economic interpretation:

  • Oil supply shock: oil price rises, industrial production falls, and core CPI rises on impact.
  • Aggregate supply shock: oil price is unrestricted, industrial production rises, and core CPI falls on impact.
  • Aggregate demand shock: oil price, industrial production, and core CPI all rise on impact.

For the IRFs, the results are rescaled so that the oil-price response at horizon 0 is exactly 9.53 log points, which is equivalent to a 10% oil-price increase. This makes the cross-shock comparison economically meaningful because every column in the IRF chart starts from the same oil-price disturbance.

For structural scenarios, APRScenario conditions on the oil-price path only. The code sets free_shocks = c(2, 3), which means shocks 2 and 3 are treated as non-driving and are fixed to zero by default. The imposed path is therefore interpreted as being driven by the oil supply shock alone.

4. Impulse response analysis

Show code
irf <- compute_impulse_responses(post, horizon = irf_horizon)

oil_impact_median <- apply(irf[1, , 1, ], 1, median)

if (any(!is.finite(oil_impact_median)) || any(oil_impact_median == 0)) {
  stop("Cannot rescale IRFs because at least one shock has a non-finite or zero oil impact on impact")
}

irf_scale <- target_oil_impact / oil_impact_median
names(irf_scale) <- shock_names
irf_rescaled <- irf

for (shock_idx in seq_along(shock_names)) {
  irf_rescaled[, shock_idx, , ] <- irf[, shock_idx, , ] * irf_scale[shock_idx]
}

irf_rescaling_summary <- data.frame(
  shock = shock_names,
  oil_impact_median_raw = as.numeric(oil_impact_median),
  target_oil_impact = rep(target_oil_impact, length(shock_names)),
  scale_factor = as.numeric(irf_scale)
)

horizon_values <- 0:(dim(irf_rescaled)[3] - 1)
irf_summary_list <- list()
irf_counter <- 1L

for (var_idx in seq_along(var_names)) {
  for (shock_idx in seq_along(shock_names)) {
    draw_matrix <- irf_rescaled[var_idx, shock_idx, , ]
    irf_summary_list[[irf_counter]] <- summarise_draws(draw_matrix, horizon_values) |>
      mutate(
        variable = unname(var_labels[var_names[var_idx]]),
        shock = shock_names[shock_idx]
      )
    irf_counter <- irf_counter + 1L
  }
}

irf_df <- bind_rows(irf_summary_list) |>
  mutate(
    variable = factor(variable, levels = unname(var_labels[var_names])),
    shock = factor(shock, levels = shock_names)
  )

oil_supply_ip_impact <- irf_df |>
  filter(variable == "Industrial production", shock == "Oil supply shock", horizon == 0) |>
  pull(median)

oil_supply_cpi_peak <- irf_df |>
  filter(variable == "Core CPI", shock == "Oil supply shock") |>
  slice_max(order_by = median, n = 1) |>
  select(horizon, median)

aggregate_supply_ip_impact <- irf_df |>
  filter(variable == "Industrial production", shock == "Aggregate supply shock", horizon == 0) |>
  pull(median)

aggregate_demand_ip_impact <- irf_df |>
  filter(variable == "Industrial production", shock == "Aggregate demand shock", horizon == 0) |>
  pull(median)

aggregate_demand_cpi_peak <- irf_df |>
  filter(variable == "Core CPI", shock == "Aggregate demand shock") |>
  slice_max(order_by = median, n = 1) |>
  select(horizon, median)

The impulse responses address a simple question: if oil rises by 10% on impact, how do China’s industrial production and core CPI respond under different structural interpretations of that increase? Because every column is normalized to the same oil-price jump, differences across panels reflect the source of the shock rather than the size of the oil move.

Three messages stand out from the results.

  • Under an oil supply shock, industrial production drops immediately by about -1.65 log points on impact, while core CPI rises only gradually and peaks at about 0.07 log points around month 5.
  • Under an aggregate supply shock, the same 10% oil-price increase is expansionary for activity on impact, with industrial production rising by about 1.31 log points and core CPI declining.
  • Under an aggregate demand shock, the oil move is associated with the strongest activity response: industrial production rises by about 4.26 log points on impact, and core CPI builds to a peak of about 0.52 log points around month 11.

In short, an oil-price increase is not informative by itself. What matters is whether the rise is supply-driven, demand-driven, or associated with broader productive conditions. In the current context, the oil-supply interpretation is the most relevant benchmark because it combines higher oil prices with weaker activity and firmer inflation.

Show code
ggplot(irf_df, aes(x = horizon, y = median)) +
  geom_hline(yintercept = 0, linewidth = 0.4, colour = "#718096") +
  geom_ribbon(aes(ymin = lower, ymax = upper), fill = "#90cdf4", alpha = 0.35) +
  geom_line(linewidth = 0.85, colour = "#1a365d") +
  facet_grid(variable ~ shock, scales = "free_y") +
  scale_x_continuous(breaks = pretty_breaks(6)) +
  labs(
    title = "Impulse responses under three structural shocks",
    subtitle = "Each column is rescaled so that the oil-price impact at horizon 0 equals a 10% increase",
    x = "Months after shock",
    y = "Response (100 * log points)"
  )

Impulse responses to a common 10% oil-price increase. Lines show posterior medians; shaded bands show 68% posterior intervals.

5. Structural scenario analysis

Show code
forecast_horizon <- 10L
forecast_posterior <- forecast(post, horizon = forecast_horizon)
forecast_draws <- forecast_posterior$forecasts
historical_dates <- data_log_sa$date
forecast_dates <- seq(
  from = seq(historical_dates[length(historical_dates)], by = "month", length.out = 2)[2],
  by = "month",
  length.out = forecast_horizon
)

yoy_from_levels <- function(x) {
  c(rep(NA_real_, 12), x[13:length(x)] - x[1:(length(x) - 12)])
}

forecast_probability <- 0.68
forecast_alpha <- (1 - forecast_probability) / 2
forecast_summary_list <- vector("list", length(var_names))
names(forecast_summary_list) <- var_names

for (var_idx in seq_along(var_names)) {
  historical_levels <- model_data[, var_idx]
  historical_yoy <- yoy_from_levels(historical_levels)
  n_draws <- dim(forecast_draws)[3]
  forecast_yoy_draws <- matrix(NA_real_, nrow = n_draws, ncol = forecast_horizon)

  for (draw_idx in seq_len(n_draws)) {
    combined_levels <- c(historical_levels, forecast_draws[var_idx, , draw_idx])
    combined_yoy <- yoy_from_levels(combined_levels)
    forecast_yoy_draws[draw_idx, ] <- tail(combined_yoy, forecast_horizon)
  }

  forecast_yoy_median <- apply(forecast_yoy_draws, 2, median)
  forecast_yoy_lower <- apply(forecast_yoy_draws, 2, stats::quantile, probs = forecast_alpha)
  forecast_yoy_upper <- apply(forecast_yoy_draws, 2, stats::quantile, probs = 1 - forecast_alpha)

  forecast_summary_list[[var_idx]] <- data.frame(
    date = forecast_dates,
    variable = var_names[var_idx],
    historical_yoy_last = historical_yoy[length(historical_yoy)],
    forecast_yoy_median = as.numeric(forecast_yoy_median),
    forecast_yoy_lower = as.numeric(forecast_yoy_lower),
    forecast_yoy_upper = as.numeric(forecast_yoy_upper)
  )
}

forecast_yoy_summary <- do.call(rbind, forecast_summary_list)
Show code
scenario_horizon <- 10L
scenario_obs <- 1L
scenario_non_driving_shocks <- c(2L, 3L)
scenario_probability <- 0.68
scenario_alpha <- (1 - scenario_probability) / 2
scenario_n_sample <- min(500L, dim(post$posterior$B)[3])

scenario_definitions <- list(
  list(
    name = "oil price stay at 100",
    file_stub = "stay_at_100",
    path = c(rep(100 * log(100), scenario_horizon))
  ),
  list(
    name = "oil price return to 80 in mid-2026",
    file_stub = "return_to_80_mid_2026",
    path = c(
      100 * log(100),
      100 * log(100),
      100 * log(100),
      100 * log(90),
      100 * log(80),
      100 * log(80),
      100 * log(80),
      100 * log(80),
      100 * log(80),
      100 * log(80)
    )
  )
)

scenario_path_table <- data.frame(
  Scenario = unname(scenario_labels[c(
    "oil price stay at 100",
    "oil price return to 80 in mid-2026"
  )]),
  `Oil-price path imposed on the model` = c(
    "100 from March 2026 through December 2026",
    "100 in March-May 2026, 90 in June 2026, and 80 from July 2026 through December 2026"
  ),
  `Driving shock` = rep("Oil supply shock", 2),
  check.names = FALSE
)

reshape_apr_draws <- function(draw_matrix, var_names, horizon) {
  n_vars <- length(var_names)
  n_draws <- ncol(draw_matrix)
  draw_array <- array(
    NA_real_,
    dim = c(n_vars, horizon, n_draws),
    dimnames = list(var_names, paste0("h", seq_len(horizon)), NULL)
  )

  for (h_idx in seq_len(horizon)) {
    row_idx <- ((h_idx - 1) * n_vars + 1):(h_idx * n_vars)
    draw_array[, h_idx, ] <- draw_matrix[row_idx, , drop = FALSE]
  }

  draw_array
}

apr_matrices <- gen_mats(posterior = post, specification = spec)
apr_unconditional <- forecast(post, horizon = scenario_horizon)
scenario_dates <- seq(
  from = seq(historical_dates[length(historical_dates)], by = "month", length.out = 2)[2],
  by = "month",
  length.out = scenario_horizon
)

aprscenario_results <- suppressWarnings(lapply(scenario_definitions, function(scenario_def) {
  set.seed(123)
  apr_scenario <- suppressWarnings(scenarios(
    h = scenario_horizon,
    path = matrix(scenario_def$path, nrow = 1),
    obs = scenario_obs,
    free_shocks = scenario_non_driving_shocks,
    n_sample = scenario_n_sample,
    posterior = post,
    matrices = apr_matrices
  ))

  scenario_draw_matrix <- suppressWarnings(simulate_conditional_forecasts(
    mu_y = apr_scenario$mu_y,
    Sigma_y = apr_scenario$Sigma_y,
    varnames = var_names,
    n_sim = 1L
  ))
  scenario_draws <- reshape_apr_draws(
    draw_matrix = scenario_draw_matrix,
    var_names = var_names,
    horizon = scenario_horizon
  )

  unconditional_draws <- apr_unconditional$forecasts[, , apr_scenario$draws_used, drop = FALSE]
  difference_draws <- scenario_draws - unconditional_draws

  list(
    name = scenario_def$name,
    file_stub = scenario_def$file_stub,
    path = scenario_def$path,
    apr_scenario = apr_scenario,
    scenario_draws = scenario_draws,
    unconditional_draws = unconditional_draws,
    difference_draws = difference_draws
  )
}))
names(aprscenario_results) <- vapply(scenario_definitions, function(x) x$name, character(1))

scenario_summary_rows <- list()

for (var_idx in seq_along(var_names)) {
  for (scenario_name in names(aprscenario_results)) {
    difference_median <- apply(
      aprscenario_results[[scenario_name]]$difference_draws[var_idx, , , drop = FALSE],
      2,
      median
    )

    scenario_summary_rows[[length(scenario_summary_rows) + 1]] <- data.frame(
      date = scenario_dates,
      variable = var_names[var_idx],
      scenario_name = scenario_name,
      unconditional_median = as.numeric(apply(aprscenario_results[[scenario_name]]$unconditional_draws[var_idx, , , drop = FALSE], 2, median)),
      scenario_median = as.numeric(apply(aprscenario_results[[scenario_name]]$scenario_draws[var_idx, , , drop = FALSE], 2, median)),
      difference_median = as.numeric(difference_median)
    )
  }
}

aprscenario_summary <- do.call(rbind, scenario_summary_rows)

scenario_variable_map <- c(ip = 2L, cpi = 3L)
scenario_summary_list <- list()
scenario_counter <- 1L

for (scenario_name in names(aprscenario_results)) {
  for (var_code in names(scenario_variable_map)) {
    draw_matrix <- aprscenario_results[[scenario_name]]$difference_draws[scenario_variable_map[[var_code]], , ]
    scenario_summary_list[[scenario_counter]] <- summarise_draws(draw_matrix, seq_len(scenario_horizon)) |>
      mutate(
        date = scenario_dates,
        variable = unname(var_labels[var_code]),
        scenario = unname(scenario_labels[scenario_name])
      )
    scenario_counter <- scenario_counter + 1L
  }
}

scenario_df <- bind_rows(scenario_summary_list) |>
  mutate(
    variable = factor(variable, levels = unname(var_labels[c("ip", "cpi")])),
    scenario = factor(
      scenario,
      levels = unname(scenario_labels[c(
        "oil price stay at 100",
        "oil price return to 80 in mid-2026"
      )])
    )
  )

scenario_stay_label <- unname(scenario_labels[["oil price stay at 100"]])
scenario_return_label <- unname(scenario_labels[["oil price return to 80 in mid-2026"]])

scenario_stay_ip_h1 <- scenario_df |>
  filter(variable == "Industrial production", scenario == scenario_stay_label, horizon == 1) |>
  pull(median)

scenario_stay_ip_h10 <- scenario_df |>
  filter(variable == "Industrial production", scenario == scenario_stay_label, horizon == 10) |>
  pull(median)

scenario_stay_cpi_h1 <- scenario_df |>
  filter(variable == "Core CPI", scenario == scenario_stay_label, horizon == 1) |>
  pull(median)

scenario_stay_cpi_h10 <- scenario_df |>
  filter(variable == "Core CPI", scenario == scenario_stay_label, horizon == 10) |>
  pull(median)

scenario_return_ip_h10 <- scenario_df |>
  filter(variable == "Industrial production", scenario == scenario_return_label, horizon == 10) |>
  pull(median)

scenario_return_cpi_h10 <- scenario_df |>
  filter(variable == "Core CPI", scenario == scenario_return_label, horizon == 10) |>
  pull(median)

Structural scenario analysis goes one step further than the IRFs. Instead of focusing on a one-time disturbance, it imposes an entire future oil-price path and asks how China’s macroeconomic variables evolve when that path is attributed to the oil supply shock. The scenario begins in March 2026, immediately after the final data point in the estimation sample, and runs through December 2026.

Two paths are considered.

Show code
kable(scenario_path_table, align = c("l", "l", "l"))
Scenario Oil-price path imposed on the model Driving shock
Oil stays at 100 from Mar-Dec 2026 100 from March 2026 through December 2026 Oil supply shock
Oil returns to 80 from Jul 2026 100 in March-May 2026, 90 in June 2026, and 80 from July 2026 through December 2026 Oil supply shock

The chart below focuses on the variables most relevant for China’s domestic economy: industrial production and core CPI. It plots the scenario effect relative to the model’s unconditional forecast, so values below zero indicate weaker activity than in the baseline and values above zero indicate stronger inflation than in the baseline.

The results are economically intuitive.

  • If oil stays at 100 from March 2026 through December 2026, industrial production is about -5.69 log points below the unconditional forecast in the first month and still about -4.64 log points lower by month 10. Core CPI is about 0.56 log points above the unconditional forecast in the first month, but it gradually returns to baseline by month 10.
  • If oil returns to 80 from July 2026 onward, the initial activity hit is similar, but the drag fades materially. By month 10, industrial production is slightly above baseline at about 0.65 log points, while core CPI is slightly lower than baseline at about -0.21 log points.

The key implication is that persistence matters. Under an oil-supply interpretation, a temporary oil-price spike imposes a front-loaded hit to activity and a modest increase in underlying inflation, but a sustained oil-price plateau keeps the growth drag in place for much longer. The uncertainty bands widen at longer horizons, so the late-sample effects should be interpreted with caution, but the difference between the two paths is still economically informative.

Show code
ggplot(scenario_df, aes(x = date, y = median, colour = scenario, fill = scenario)) +
  geom_hline(yintercept = 0, linewidth = 0.4, colour = "#718096") +
  geom_ribbon(aes(ymin = lower, ymax = upper), alpha = 0.18, colour = NA) +
  geom_line(linewidth = 0.95) +
  facet_wrap(~ variable, ncol = 1, scales = "free_y") +
  scale_colour_manual(values = scenario_colors) +
  scale_fill_manual(values = scenario_colors) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b\n%Y") +
  labs(
    title = "Structural scenario effects on China relative to the unconditional forecast",
    subtitle = "Oil path imposed on the oil-price variable; shocks 2 and 3 are constrained to zero",
    x = NULL,
    y = "Scenario minus unconditional (100 * log points)",
    colour = "Oil path",
    fill = "Oil path"
  )

Structural oil-supply scenarios for China. The figure shows scenario minus unconditional forecast and focuses only on industrial production and core CPI, as requested.