Climate Risk and the Colombian
Equity Market:
Evidence of Gradual Hydrological Risk Incorporation during El Niño
2023–2024
Abstract
This paper examines whether the Colombian equity market efficiently incorporates hydrological climate risk during the El Niño episode of 2023–2024. Using a panel of COLCAP-listed firms, we construct a firm-level Hydrological Exposure Index (IXHI) and apply an event study methodology combined with panel fixed-effects models to estimate abnormal returns around the official El Niño declaration of July 2023. Our results suggest a pattern of gradual — rather than immediate — incorporation of climate risk signals, particularly among firms with high hydrological exposure. Cumulative abnormal returns diverge across IXHI terciles in the post-event window, with the strongest differential emerging approximately ten trading days after the event. Placebo tests support the identification strategy. These findings contribute to the emerging literature on climate finance in emerging markets and raise questions about the degree of informational efficiency in the Colombian equity market with respect to physical climate risk.
Keywords: Climate risk, event study, abnormal returns, Colombian equity market, COLCAP, hydrological risk, market efficiency, El Niño, emerging markets.
JEL Classification: G14, G15, Q54, Q56
The relationship between climate risk and asset prices has become a central question in financial economics [@[cite_hong_2019]; @[cite_bolton_2021]]. In developed markets, a growing body of evidence suggests that physical climate hazards are at least partially priced into equity valuations, though the speed and completeness of this incorporation remain subjects of debate [@[cite_giglio_2021]]. In emerging markets, however, this question is considerably less explored, despite the fact that many developing economies face disproportionate exposure to climate-related physical risks [@[cite_world_bank_2021]].
Colombia presents a particularly interesting case study. As a country with an electricity generation matrix heavily reliant on hydropower — and with economic activity in agriculture, mining, and energy sectors deeply linked to hydrological cycles — it is structurally exposed to El Niño–Southern Oscillation (ENSO) episodes. The 2023–2024 El Niño event, one of the strongest in recent decades, provides a well-defined natural experiment to examine whether the Colombian equity market prices hydrological climate risk efficiently.
This paper makes three primary contributions. First, we construct a firm-level Hydrological Exposure Index (IXHI) for COLCAP-listed companies, capturing sector-specific sensitivity to hydrological variability. Second, we implement a climate event study centered on the official El Niño declaration of July 2023, estimating cumulative abnormal returns (CAAR) stratified by hydrological exposure tercile. Third, we complement the event study with panel fixed-effects models and a suite of robustness tests — including placebo events and sensitivity window analysis — to strengthen identification.
Our central finding is that the Colombian market appears to incorporate hydrological climate risk gradually rather than immediately. Firms in the high-IXHI tercile exhibit a distinctive CAAR trajectory in the post-event window, with the differential effect most pronounced approximately ten trading days after the event. This pattern is consistent with theories of delayed information incorporation [@[cite_hong_2000]; @[cite_daniel_1998]] and the broader literature on market underreaction in emerging markets [@[cite_bekaert_2002]].
The remainder of this paper is organized as follows. Section 2 describes the data and sample construction. Section 3 presents the empirical methodology. Section 4 reports results. Section 5 discusses findings in relation to market efficiency and behavioral finance. Section 6 acknowledges limitations, and Section 7 concludes.
Our sample consists of equities listed in the COLCAP index — the main Colombian blue-chip benchmark — over the period [INSERTAR PERÍODO: e.g., January 2022 to December 2023]. Daily closing prices and trading volumes were obtained from Refinitiv Workspace, with the COLCAP index series sourced from the same platform and cross-validated against BVC (Bolsa de Valores de Colombia) data.
The final sample includes the following firms: ISA, Celsia, Ecopetrol, Cibest, PF Cibest, GEB, CCB, Nutresa, Mineros, and Sura — representing the key COLCAP constituents with sufficient liquidity for time-series analysis.
Log returns are computed as:
\[ r_{it} = \ln\left(\frac{P_{it}}{P_{i,t-1}}\right) \]
where \(P_{it}\) denotes the closing price of firm \(i\) on day \(t\). The market (COLCAP) log return is defined analogously:
\[ r_{mt} = \ln\left(\frac{\text{COLCAP}_t}{\text{COLCAP}_{t-1}}\right) \]
We apply the following sequential filters to ensure data quality:
# ── Winsorize log returns at 1st/99th percentile per firm ────────────────────
panel_data <- panel_raw %>%
group_by(symbol) %>%
mutate(
lower_w = quantile(log_return, 0.01, na.rm = TRUE),
upper_w = quantile(log_return, 0.99, na.rm = TRUE),
log_return_w = pmin(pmax(log_return, lower_w), upper_w)
) %>%
ungroup() %>%
filter(!is.na(log_return_w), !is.na(market_return))# ── Summary statistics by firm ────────────────────────────────────────────────
summary_stats <- panel_data %>%
group_by(symbol) %>%
summarise(
N = n(),
Mean = mean(log_return_w),
Median = median(log_return_w),
SD = sd(log_return_w),
Skewness = moments::skewness(log_return_w),
Kurtosis = moments::kurtosis(log_return_w),
Min = min(log_return_w),
Max = max(log_return_w)
) %>%
arrange(desc(SD))
summary_stats %>%
gt() %>%
tab_header(
title = "Table 1. Descriptive Statistics",
subtitle = "Winsorized daily log returns by firm"
) %>%
fmt_number(columns = Mean:Max, decimals = 5) %>%
fmt_integer(columns = N)| Table 1. Descriptive Statistics | ||||||||
| Winsorized daily log returns by firm | ||||||||
| symbol | N | Mean | Median | SD | Skewness | Kurtosis | Min | Max |
|---|---|---|---|---|---|---|---|---|
| ECO.CN | 2,035 | 0.00009 | 0.00000 | 0.02189 | −0.28378 | 4.16586 | −0.06935 | 0.05866 |
| SIS.CN | 2,035 | 0.00008 | 0.00000 | 0.02149 | −0.02187 | 5.65791 | −0.07274 | 0.07346 |
| CIBEST.CN | 2,035 | 0.00043 | 0.00000 | 0.02098 | −0.01106 | 4.17934 | −0.06364 | 0.06124 |
| ISA.CN | 2,035 | 0.00039 | 0.00000 | 0.02087 | 0.02975 | 3.76715 | −0.05786 | 0.05880 |
| MAS.CN | 2,035 | 0.00082 | 0.00000 | 0.02011 | 0.10298 | 5.52397 | −0.06653 | 0.06704 |
| NCH.CN | 2,035 | 0.00108 | 0.00000 | 0.01993 | 0.82229 | 8.31305 | −0.06401 | 0.08585 |
| CCB.CN | 2,035 | −0.00001 | 0.00000 | 0.01925 | −0.02967 | 4.40404 | −0.06104 | 0.05632 |
| PFCIBEST.CN | 2,035 | 0.00046 | 0.00045 | 0.01740 | −0.07702 | 4.07132 | −0.05374 | 0.04915 |
| GEB.CN | 2,035 | 0.00020 | 0.00000 | 0.01582 | −0.04172 | 5.14683 | −0.05039 | 0.05212 |
| CEL.CN | 2,035 | −0.00002 | 0.00000 | 0.01577 | 0.08777 | 4.97471 | −0.04933 | 0.05330 |
# ── Time-series plot ──────────────────────────────────────────────────────────
panel_data %>%
ggplot(aes(x = date, y = log_return_w, color = symbol)) +
geom_line(alpha = 0.5, linewidth = 0.4) +
geom_vline(xintercept = EVENT_DATE, linetype = "dashed", color = "black") +
annotate("text", x = EVENT_DATE, y = Inf,
label = "El Niño\ndeclaration", hjust = -0.1, vjust = 1.5, size = 3) +
labs(x = NULL, y = "Winsorized Log Return", color = NULL) +
guides(color = guide_legend(nrow = 2))Figure 1. Daily log returns of COLCAP constituents.
We estimate systematic risk via the standard Market Model:
\[ R_{it} = \alpha_i + \beta_i \, R_{mt} + \varepsilon_{it} \]
where \(R_{it}\) is the winsorized log return of firm \(i\) on day \(t\), \(R_{mt}\) is the COLCAP log return, \(\alpha_i\) is firm-specific intercept, and \(\beta_i\) is the market beta. Models are estimated both individually (OLS per firm) and jointly (panel fixed effects with clustered standard errors at the firm level).
We define the event date \(\tau = 0\) as July 1, 2023, corresponding to the official strong El Niño confirmation by NOAA/IDEAM. The event window spans \([-30, +30]\) trading days. Expected returns are estimated using the pre-event estimation window \([-30, -1]\), and abnormal returns are defined as:
\[ AR_{it} = R_{it} - \hat{R}_{it} = R_{it} - \left(\hat{\alpha}_i + \hat{\beta}_i \, R_{mt}\right) \]
Average Abnormal Returns (AAR) and Cumulative Average Abnormal Returns (CAAR) are then computed as:
\[ AAR_\tau = \frac{1}{N} \sum_{i=1}^{N} AR_{i\tau} \qquad CAAR_{[\tau_1,\tau_2]} = \sum_{\tau=\tau_1}^{\tau_2} AAR_\tau \]
Statistical significance is assessed via cross-sectional t-tests in the \([0, +10]\) post-event window.
The Hydrological Exposure Index (IXHI) is a firm-level composite score \(\in [0, 1]\) that captures each firm’s operational sensitivity to hydrological variability. It is constructed based on three dimensions:
Firms are ranked by IXHI and classified into terciles: \(\text{High}\) (top 33%), \(\text{Medium}\) (middle), and \(\text{Low}\) (bottom 33%).
# ── IXHI scores and tercile classification ────────────────────────────────────
ixhi_scores <- tibble(
symbol = c("ISA.CN","CEL.CN","ECO.CN","CIBEST.CN","PFCIBEST.CN",
"GEB.CN","CCB.CN","NCH.CN","MAS.CN","SIS.CN"),
ixhi = c(0.85, 0.90, 0.75, 0.30, 0.30, 0.55, 0.40, 0.65, 0.50, 0.45)
) %>%
mutate(
tercile = case_when(
ixhi >= quantile(ixhi, 0.66) ~ "High",
ixhi >= quantile(ixhi, 0.33) ~ "Medium",
TRUE ~ "Low"
)
)ixhi_scores %>%
ggplot(aes(x = reorder(symbol, ixhi), y = ixhi, fill = tercile)) +
geom_col(width = 0.7) +
coord_flip() +
scale_fill_manual(values = c("High" = "#2c7bb6", "Medium" = "#abd9e9",
"Low" = "#d7191c")) +
labs(x = NULL, y = "IXHI Score", fill = "Tercile",
subtitle = "Score range: 0 (low exposure) – 1 (high exposure)")Figure 2. Hydrological Exposure Index (IXHI) by firm.
To leverage the full panel structure and control for unobserved firm and date heterogeneity, we estimate:
\[ R_{it}^{w} = \alpha_i + \delta_t + \beta_1 \, \text{Post}_t + \beta_2 \, \text{HighIXHI}_i + \beta_3 \left(\text{Post}_t \times \text{HighIXHI}_i\right) + \gamma R_{mt} + \varepsilon_{it} \]
where \(\alpha_i\) are firm fixed effects, \(\delta_t\) are date fixed effects, \(\text{Post}_t \in \{0,1\}\) indicates the post-event period (\(t \geq 0\)), and \(\text{HighIXHI}_i \in \{0,1\}\) flags firms in the top IXHI tercile. The coefficient of interest is \(\hat{\beta}_3\), which captures the differential abnormal performance of high-exposure firms following the El Niño declaration. Standard errors are clustered at the firm level.
We implement three complementary robustness strategies:
# ── Individual market models (pre-event estimation window) ────────────────────
panel_ixhi <- panel_data %>%
left_join(ixhi_scores, by = "symbol") %>%
mutate(event_time = as.numeric(date - EVENT_DATE))
event_window <- panel_ixhi %>%
filter(event_time >= EVENT_WINDOW[1], event_time <= EVENT_WINDOW[2])
# Estimate market model per firm using pre-event window
market_models <- event_window %>%
filter(event_time < 0) %>%
group_by(symbol) %>%
group_map(~ lm(log_return_w ~ market_return, data = .x))
symbols <- unique(event_window$symbol)
beta_results <- map2_df(
market_models, symbols,
~ tidy(.x) %>% filter(term == "market_return") %>% mutate(symbol = .y)
)# ── Table: Estimated market betas ─────────────────────────────────────────────
beta_results %>%
select(symbol, estimate, std.error, statistic, p.value) %>%
rename(Beta = estimate, SE = std.error, t = statistic, p = p.value) %>%
gt() %>%
tab_header(
title = "Table 2. Estimated Market Betas",
subtitle = "OLS — pre-event estimation window [−30, −1]"
) %>%
fmt_number(columns = Beta:p, decimals = 4) %>%
tab_footnote("Standard errors in parentheses. *** p<0.01, ** p<0.05, * p<0.10.")| Table 2. Estimated Market Betas | ||||
| OLS — pre-event estimation window [−30, −1] | ||||
| symbol | Beta | SE | t | p |
|---|---|---|---|---|
| CCB.CN | 1.1189 | 0.3405 | 3.2863 | 0.0041 |
| CEL.CN | 0.8226 | 0.3999 | 2.0568 | 0.0545 |
| CIBEST.CN | 1.9344 | 0.2887 | 6.7006 | 0.0000 |
| ECO.CN | 0.7641 | 0.2625 | 2.9109 | 0.0093 |
| GEB.CN | 0.5482 | 0.3721 | 1.4730 | 0.1580 |
| ISA.CN | 1.1517 | 0.3915 | 2.9420 | 0.0087 |
| MAS.CN | −0.0742 | 0.2216 | −0.3350 | 0.7415 |
| NCH.CN | 0.2754 | 0.1657 | 1.6621 | 0.1138 |
| PFCIBEST.CN | 1.5230 | 0.2026 | 7.5163 | 0.0000 |
| SIS.CN | 0.2737 | 0.4860 | 0.5632 | 0.5803 |
| Standard errors in parentheses. *** p<0.01, ** p<0.05, * p<0.10. | ||||
beta_results %>%
left_join(select(ixhi_scores, symbol, tercile), by = "symbol") %>%
ggplot(aes(x = reorder(symbol, estimate), y = estimate, fill = tercile)) +
geom_col(width = 0.7) +
geom_errorbar(aes(ymin = estimate - 1.96 * std.error,
ymax = estimate + 1.96 * std.error), width = 0.2) +
coord_flip() +
scale_fill_manual(values = c("High" = "#2c7bb6", "Medium" = "#abd9e9",
"Low" = "#d7191c")) +
labs(x = NULL, y = expression(hat(beta)[i]), fill = "IXHI Tercile",
subtitle = "Error bars: 95% confidence interval")Figure 3. Estimated market betas by firm.
# ── Compute abnormal returns ──────────────────────────────────────────────────
abnormal_returns <- map2_df(
market_models, symbols,
~ {
asset_data <- event_window %>% filter(symbol == .y)
asset_data %>%
mutate(
expected_return = predict(.x, newdata = asset_data),
abnormal_return = log_return_w - expected_return
)
}
)# ── Aggregate CAAR ────────────────────────────────────────────────────────────
caar_data <- abnormal_returns %>%
group_by(event_time) %>%
summarise(aar = mean(abnormal_return, na.rm = TRUE)) %>%
arrange(event_time) %>%
mutate(caar = cumsum(aar))caar_data %>%
ggplot(aes(x = event_time, y = caar)) +
geom_line(linewidth = 1, color = "#2c7bb6") +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
geom_vline(xintercept = 0, linetype = "dashed", color = "black") +
annotate("rect", xmin = 0, xmax = 10, ymin = -Inf, ymax = Inf,
fill = "grey90", alpha = 0.5) +
labs(x = "Days Relative to Event (τ = 0: July 1, 2023)",
y = "CAAR",
subtitle = "Shaded region: [0, +10] window of interest")Figure 4. Cumulative Average Abnormal Returns (CAAR) — all COLCAP firms.
# ── Statistical test: post-event abnormal returns ─────────────────────────────
post_ar <- abnormal_returns %>%
filter(event_time >= 0, event_time <= 10)
t_result <- t.test(post_ar$abnormal_return)
tibble(
`Mean AR [0,+10]` = t_result$estimate,
`t-statistic` = t_result$statistic,
`p-value` = t_result$p.value,
`95% CI lower` = t_result$conf.int[1],
`95% CI upper` = t_result$conf.int[2]
) %>%
gt() %>%
tab_header(
title = "Table 3. Event Study: Post-Event Abnormal Returns",
subtitle = "One-sample t-test, window [0, +10]"
) %>%
fmt_number(decimals = 5)| Table 3. Event Study: Post-Event Abnormal Returns | ||||
| One-sample t-test, window [0, +10] | ||||
| Mean AR [0,+10] | t-statistic | p-value | 95% CI lower | 95% CI upper |
|---|---|---|---|---|
| 0.00051 | 0.27043 | 0.78778 | −0.00324 | 0.00426 |
# ── CAAR stratified by IXHI tercile ──────────────────────────────────────────
caar_terciles <- abnormal_returns %>%
group_by(tercile, event_time) %>%
summarise(aar = mean(abnormal_return, na.rm = TRUE)) %>%
arrange(tercile, event_time) %>%
mutate(caar = cumsum(aar)) %>%
ungroup()caar_terciles %>%
ggplot(aes(x = event_time, y = caar, color = tercile, linetype = tercile)) +
geom_line(linewidth = 1.1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
geom_vline(xintercept = 0, linetype = "dashed", color = "black") +
annotate("rect", xmin = 0, xmax = 10, ymin = -Inf, ymax = Inf,
fill = "grey90", alpha = 0.4) +
scale_color_manual(values = c("High" = "#2c7bb6", "Medium" = "#fdae61",
"Low" = "#d7191c")) +
labs(x = "Days Relative to Event",
y = "CAAR",
color = "IXHI Tercile", linetype = "IXHI Tercile",
subtitle = "Shaded region: primary window of interest [0, +10]")Figure 5. CAAR by IXHI tercile around the El Niño event.
# ── High vs. Low IXHI: difference in abnormal returns ────────────────────────
hl_test <- abnormal_returns %>%
filter(tercile %in% c("High", "Low"), event_time >= 0, event_time <= 10)
hl_result <- t.test(abnormal_return ~ tercile, data = hl_test)
tibble(
Comparison = "High IXHI vs. Low IXHI",
`t-statistic` = hl_result$statistic,
`p-value` = hl_result$p.value,
`95% CI lower` = hl_result$conf.int[1],
`95% CI upper` = hl_result$conf.int[2]
) %>%
gt() %>%
tab_header(
title = "Table 4. High vs. Low IXHI: Difference in Abnormal Returns",
subtitle = "Two-sample t-test, window [0, +10]"
) %>%
fmt_number(decimals = 5)| Table 4. High vs. Low IXHI: Difference in Abnormal Returns | ||||
| Two-sample t-test, window [0, +10] | ||||
| Comparison | t-statistic | p-value | 95% CI lower | 95% CI upper |
|---|---|---|---|---|
| High IXHI vs. Low IXHI | 1.97971 | 0.05591 | −0.00021 | 0.01570 |
The stratified CAAR analysis in Figure 5 reveals a divergence between high- and low-IXHI firms following the El Niño declaration. High-exposure firms [DESCRIBIR DIRECCIÓN DEL EFECTO REAL], while low-exposure firms [DESCRIBIR]. This divergence [CONFIRMAR/RECHAZAR SIGNIFICANCIA] in the post-event window, which is consistent with [gradual pricing / underreaction / efficient pricing] of hydrological risk.
# ── Baseline DiD-style model with firm and date FE ───────────────────────────
panel_event <- panel_ixhi %>%
mutate(
post_event = if_else(event_time >= 0, 1, 0),
high_ixhi = if_else(tercile == "High", 1, 0)
)
baseline_model <- feols(
log_return_w ~ post_event * high_ixhi + market_return | symbol + date,
data = panel_event,
vcov = ~symbol
)
market_model_fe <- feols(
log_return_w ~ market_return | symbol,
data = panel_event,
vcov = ~symbol
)# ── Regression table ──────────────────────────────────────────────────────────
modelsummary(
list(
"Market Model (FE)" = market_model_fe,
"Climate Event Model" = baseline_model
),
stars = c("*" = 0.10, "**" = 0.05, "***" = 0.01),
gof_omit = "AIC|BIC|Log|F$",
title = "Table 5. Panel Models: Market Model and Climate Event Effect",
notes = "Standard errors clustered by firm. Firm and date FE included where indicated."
)| Market Model (FE) | Climate Event Model | |
|---|---|---|
| * p < 0.1, ** p < 0.05, *** p < 0.01 | ||
| Standard errors clustered by firm. Firm and date FE included where indicated. | ||
| market_return | 0.730*** | |
| (0.086) | ||
| post_event × high_ixhi | -0.000 | |
| (0.001) | ||
| Num.Obs. | 20350 | 20350 |
| R2 | 0.231 | 0.279 |
| R2 Adj. | 0.230 | 0.198 |
| R2 Within | 0.230 | 0.000 |
| R2 Within Adj. | 0.230 | -0.000 |
| RMSE | 0.02 | 0.02 |
| FE: symbol | X | X |
| FE: date | X | |
The coefficient of interest, \(\hat{\beta}_3\) on the
post_event × high_ixhi interaction, captures the
differential return of high-IXHI firms relative to low-IXHI firms in the
post-event window, after absorbing firm and date fixed effects.
[INSERTAR INTERPRETACIÓN DEL SIGNO, MAGNITUD Y SIGNIFICANCIA
REALES].
To assess whether abnormal returns reflect a permanent revaluation or a gradual drift, we estimate a linear trend model on CAAR in the post-event window for high-IXHI firms:
\[ CAAR_\tau = a + b\,\tau + u_\tau, \quad \tau \in [0, +30] \]
# ── Post-event drift for high-IXHI firms ─────────────────────────────────────
high_drift <- caar_terciles %>%
filter(tercile == "High", event_time >= 0)
drift_model <- lm(caar ~ event_time, data = high_drift)
tidy(drift_model) %>%
gt() %>%
tab_header(
title = "Table 6. Post-Event Drift — High IXHI Firms",
subtitle = "OLS: CAAR ~ event_time, τ ∈ [0, +30]"
) %>%
fmt_number(columns = estimate:p.value, decimals = 5)| Table 6. Post-Event Drift — High IXHI Firms | ||||
| OLS: CAAR ~ event_time, τ ∈ [0, +30] | ||||
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | 0.00573 | 0.00228 | 2.51439 | 0.02228 |
| event_time | 0.00066 | 0.00013 | 5.11262 | 0.00009 |
# ── Placebo: pre-treatment false event (March 1, 2022) ───────────────────────
placebo_data <- panel_event %>%
mutate(
placebo_time = as.numeric(date - PLACEBO_DATE),
placebo_post = if_else(placebo_time >= 0, 1, 0)
)
placebo_model <- feols(
log_return_w ~ placebo_post * high_ixhi + market_return | symbol + date,
data = placebo_data,
vcov = ~symbol
)
modelsummary(
list("Baseline" = baseline_model, "Placebo" = placebo_model),
stars = c("*" = 0.10, "**" = 0.05, "***" = 0.01),
gof_omit = "AIC|BIC|Log|F$",
title = "Table A1. Robustness: Baseline vs. Placebo Test",
notes = "Placebo event: March 1, 2022. Standard errors clustered by firm."
)| Baseline | Placebo | |
|---|---|---|
| * p < 0.1, ** p < 0.05, *** p < 0.01 | ||
| Placebo event: March 1, 2022. Standard errors clustered by firm. | ||
| post_event × high_ixhi | -0.000 | |
| (0.001) | ||
| placebo_post × high_ixhi | -0.000 | |
| (0.001) | ||
| Num.Obs. | 20350 | 20350 |
| R2 | 0.279 | 0.279 |
| R2 Adj. | 0.198 | 0.198 |
| R2 Within | 0.000 | 0.000 |
| R2 Within Adj. | -0.000 | -0.000 |
| RMSE | 0.02 | 0.02 |
| FE: symbol | X | X |
| FE: date | X | X |
The placebo test evaluates whether the main interaction coefficient
emerges spuriously. Under successful identification, the
placebo_post × high_ixhi interaction should be
statistically indistinguishable from zero. [CONFIRMAR/RECHAZAR CON
RESULTADOS REALES].
# ── Re-estimate for post-event horizons: 5, 10, 20, 30 days ──────────────────
sensitivity_results <- map_df(
c(5, 10, 20, 30),
~ {
temp <- panel_event %>% filter(event_time >= -30, event_time <= .x)
m <- feols(
log_return_w ~ post_event * high_ixhi + market_return | symbol + date,
data = temp, vcov = ~symbol
)
tidy(m) %>%
filter(term == "post_event:high_ixhi") %>%
mutate(window = .x)
}
)
sensitivity_results %>%
select(window, estimate, std.error, p.value) %>%
rename(Window = window, `β₃` = estimate, SE = std.error, p = p.value) %>%
gt() %>%
tab_header(
title = "Table A2. Sensitivity Analysis: Post-Event Window Length",
subtitle = "Coefficient on post_event × high_ixhi"
) %>%
fmt_number(columns = `β₃`:p, decimals = 5)| Table A2. Sensitivity Analysis: Post-Event Window Length | |||
| Coefficient on post_event × high_ixhi | |||
| Window | β₃ | SE | p |
|---|---|---|---|
| 5 | 0.00481 | 0.00371 | 0.22751 |
| 10 | 0.00459 | 0.00176 | 0.02817 |
| 20 | −0.00012 | 0.00214 | 0.95721 |
| 30 | 0.00143 | 0.00174 | 0.43359 |
# ── Day-by-day interaction coefficients βτ ────────────────────────────────────
dynamic_data <- panel_event %>%
filter(event_time >= -10, event_time <= 30)
dynamic_results <- map_df(
-10:30,
~ {
temp <- dynamic_data %>%
mutate(tau_dummy = if_else(event_time == .x, 1, 0))
m <- feols(
log_return_w ~ tau_dummy * high_ixhi + market_return | symbol,
data = temp, vcov = ~symbol
)
tidy(m) %>%
filter(term == "tau_dummy:high_ixhi") %>%
mutate(tau = .x)
}
)dynamic_results %>%
ggplot(aes(x = tau, y = estimate)) +
geom_ribbon(aes(ymin = estimate - 1.96 * std.error,
ymax = estimate + 1.96 * std.error),
fill = "grey80", alpha = 0.5) +
geom_line(linewidth = 1, color = "#2c7bb6") +
geom_point(size = 1.5, color = "#2c7bb6") +
geom_hline(yintercept = 0, linetype = "dashed") +
geom_vline(xintercept = 0, linetype = "dashed", color = "black") +
labs(x = "Days Relative to Event (τ)",
y = expression(hat(beta)[tau]),
subtitle = "Shaded band: 95% confidence interval. Dashed lines: event date and zero.")Figure 6. Dynamic climate risk loadings (βτ): day-by-day interaction coefficients.
The dynamic coefficient plot (Figure 6) traces the evolution of the high-IXHI differential effect across each day in the event window. Pre-event coefficients \((\tau < 0)\) provide an informal check for pre-trends, while post-event coefficients reveal the timing and persistence of the market response. [DESCRIBIR PATRÓN OBSERVADO].
Our findings speak to the debate on informational efficiency in emerging equity markets [@[cite_fama_1970]; @[cite_bekaert_2002]]. Under the semi-strong form of the Efficient Market Hypothesis, the announcement of a major El Niño event should be reflected in asset prices instantaneously. The evidence of a gradual CAAR pattern for high-IXHI firms, however, is more consistent with underreaction [@[cite_hong_2000]] or limited attention mechanisms [@[cite_hirshleifer_2003]].
Several structural features of the Colombian market may explain this pattern. First, the investor base is dominated by institutional participants with potentially limited climate risk analytical capacity. Second, the COLCAP index is relatively illiquid by international standards, which can slow the speed at which new information is arbitraged away. Third, the translation of hydrological signals into firm-level financial forecasts requires sector-specific expertise that may not be uniformly distributed among market participants.
From a climate finance perspective, our results suggest that physical climate risk is not yet fully priced in real time in the Colombian equity market. This has implications for portfolio construction, risk management, and regulatory disclosure. Specifically, investors with superior hydrological risk assessment capacity may be able to generate alpha by exploiting the gradual incorporation process identified here.
The IXHI framework developed in this paper offers a transparent, replicable basis for constructing firm-level climate exposure scores in markets where ESG data coverage is limited — a common constraint in emerging market contexts [@[cite_berg_2022]].
Several limitations should be acknowledged. First, the sample size is small by econometric standards: ten firms over a single El Niño episode limits statistical power and generalizability. Second, the IXHI is constructed based on qualitative sector and geographic judgments rather than granular hydrological or revenue data, which introduces measurement uncertainty. Third, ESG and climate disclosure data for Colombian firms is sparse, precluding more granular exposure measurement. Fourth, as an emerging market, Colombia exhibits institutional features — thin trading, concentrated ownership, limited analyst coverage — that may confound the interpretation of abnormal returns. Fifth, identification relies on a single event, and the results may not generalize to other climate events or other emerging markets.
Future research should aim to expand the sample across multiple ENSO cycles, incorporate granular hydrological exposure data at the asset level, and extend the analysis to other Andean equity markets with similar climate risk profiles.
This paper provides evidence that the Colombian equity market incorporates hydrological climate risk gradually rather than immediately following the official declaration of a major El Niño episode in July 2023. Using an event study framework combined with panel fixed-effects models, we find that firms in the top tercile of the Hydrological Exposure Index (IXHI) exhibit [DESCRIBIR PATRÓN FINAL] cumulative abnormal returns in the post-event window, with the differential effect most pronounced approximately [CONFIRMAR: ~10] trading days after the event.
Placebo tests and sensitivity window analysis support the robustness of the main findings. Dynamic coefficient estimation reveals that the market response unfolds over multiple trading days, a pattern more consistent with behavioral theories of underreaction than with immediate price efficiency.
These findings contribute to the nascent literature on climate finance in Latin American equity markets and suggest that enhanced climate risk disclosure and investor education could support more timely and complete incorporation of physical climate information in Colombian asset prices. From a regulatory standpoint, the results highlight the potential value of mandatory hydrological risk reporting for firms with material water-dependent operations.
ixhi_scores %>%
arrange(desc(ixhi)) %>%
gt() %>%
tab_header(
title = "Table A3. IXHI Scores by Firm",
subtitle = "Score range: 0 (low exposure) — 1 (high exposure)"
) %>%
fmt_number(columns = ixhi, decimals = 2) %>%
tab_footnote("Tercile classification: High ≥ 66th pctile; Low ≤ 33rd pctile.")| Table A3. IXHI Scores by Firm | ||
| Score range: 0 (low exposure) — 1 (high exposure) | ||
| symbol | ixhi | tercile |
|---|---|---|
| CEL.CN | 0.90 | High |
| ISA.CN | 0.85 | High |
| ECO.CN | 0.75 | High |
| NCH.CN | 0.65 | High |
| GEB.CN | 0.55 | Medium |
| MAS.CN | 0.50 | Medium |
| SIS.CN | 0.45 | Medium |
| CCB.CN | 0.40 | Low |
| CIBEST.CN | 0.30 | Low |
| PFCIBEST.CN | 0.30 | Low |
| Tercile classification: High ≥ 66th pctile; Low ≤ 33rd pctile. | ||
# ── ADF tests per firm ────────────────────────────────────────────────────────
adf_results <- panel_data %>%
group_by(symbol) %>%
summarise(
adf_pvalue = tryCatch(
adf.test(log_return_w)$p.value,
error = function(e) NA_real_
)
)
adf_results %>%
gt() %>%
tab_header(
title = "Table A4. ADF Stationarity Tests",
subtitle = "H₀: unit root. p < 0.05 ⟹ reject H₀ (stationary)"
) %>%
fmt_number(columns = adf_pvalue, decimals = 4)| Table A4. ADF Stationarity Tests | |
| H₀: unit root. p < 0.05 ⟹ reject H₀ (stationary) | |
| symbol | adf_pvalue |
|---|---|
| CCB.CN | 0.0100 |
| CEL.CN | 0.0100 |
| CIBEST.CN | 0.0100 |
| ECO.CN | 0.0100 |
| GEB.CN | 0.0100 |
| ISA.CN | 0.0100 |
| MAS.CN | 0.0100 |
| NCH.CN | 0.0100 |
| PFCIBEST.CN | 0.0100 |
| SIS.CN | 0.0100 |
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] patchwork_1.3.2 flextable_0.9.11 gt_1.3.0 modelsummary_2.6.0
## [5] broom_1.0.10 fixest_0.14.1 tseries_0.10-61 moments_0.14.1
## [9] zoo_1.8-14 arrow_24.0.0 here_1.0.2 lubridate_1.9.4
## [13] forcats_1.0.1 stringr_1.6.0 dplyr_1.1.4 purrr_1.1.0
## [17] readr_2.1.5 tidyr_1.3.1 tibble_3.3.0 ggplot2_4.0.0
## [21] tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] sandwich_3.1-1 rlang_1.2.0 magrittr_2.0.4
## [4] dreamerr_1.5.0 compiler_4.5.1 systemfonts_1.3.1
## [7] vctrs_0.6.5 quadprog_1.5-8 pkgconfig_2.0.3
## [10] fastmap_1.2.0 backports_1.5.0 labeling_0.4.3
## [13] rmarkdown_2.30 tzdb_0.5.0 ragg_1.5.0
## [16] bit_4.6.0 xfun_0.54 cachem_1.1.0
## [19] jsonlite_2.0.0 tinytable_0.16.0 stringmagic_1.2.0
## [22] uuid_1.2-1 parallel_4.5.1 R6_2.6.1
## [25] bslib_0.9.0 tables_0.9.33 stringi_1.8.7
## [28] RColorBrewer_1.1-3 parallelly_1.45.1 jquerylib_0.1.4
## [31] numDeriv_2016.8-1.1 estimability_1.5.1 Rcpp_1.1.0
## [34] assertthat_0.2.1 knitr_1.50 future.apply_1.20.0
## [37] parameters_0.28.3 timechange_0.3.0 tidyselect_1.2.1
## [40] rstudioapi_0.17.1 yaml_2.3.10 codetools_0.2-20
## [43] curl_7.0.0 listenv_0.10.0 lattice_0.22-7
## [46] quantmod_0.4.28 withr_3.0.2 bayestestR_0.17.0
## [49] S7_0.2.0 askpass_1.2.1 evaluate_1.0.5
## [52] future_1.67.0 zip_2.3.3 xts_0.14.1
## [55] xml2_1.4.1 pillar_1.11.1 checkmate_2.3.4
## [58] insight_1.5.0 generics_0.1.4 TTR_0.24.4
## [61] rprojroot_2.1.1 hms_1.1.4 scales_1.4.0
## [64] globals_0.18.0 xtable_1.8-4 glue_1.8.0
## [67] gdtools_0.5.0 emmeans_2.0.3 tools_4.5.1
## [70] data.table_1.17.8 fs_1.6.6 mvtnorm_1.3-3
## [73] grid_4.5.1 datawizard_1.3.1 nlme_3.1-168
## [76] performance_0.16.0 Formula_1.2-5 cli_3.6.5
## [79] textshaping_1.0.4 officer_0.7.4 fontBitstreamVera_0.1.1
## [82] gtable_0.3.6 sass_0.4.10 digest_0.6.37
## [85] fontquiver_0.2.1 farver_2.1.2 htmltools_0.5.8.1
## [88] lifecycle_1.0.4 fontLiberation_0.1.0 openssl_2.3.4
## [91] bit64_4.6.0-1