Your referee would say: “Banks with high MTM losses and high uninsured deposits borrowed more. So what? That’s expected.”
| Pillar | What You Add | Why It Matters |
|---|---|---|
| **1. Structural Parameter (θ*)** | Estimate bank-specific run thresholds | Gives readers a number to cite |
| 2. Mechanism Test | Show BTFP screened for run risk, not just subsidized | Challenges the “BTFP was a bailout” narrative |
| 3. Counterfactual | Quantify “runs prevented” without BTFP | Policy-relevant welfare calculation |
| 4. Contagion Channel | Geographic/network spillovers beyond fundamentals | New insight on crisis transmission |
library(tidyverse)
library(data.table)
library(lubridate)
library(fixest)
library(modelsummary)
library(kableExtra)
library(scales)
library(ggplot2)
library(patchwork)
library(nnet)
library(broom)
cat("All packages loaded.\n")
## All packages loaded.
# ==============================================================================
# PATHS - ADJUST TO YOUR SYSTEM
# ==============================================================================
BASE_PATH <- "C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025"
DATA_PROC <- file.path(BASE_PATH, "01_data/processed")
OUTPUT_PATH <- file.path(BASE_PATH, "03_documentation/crisis_borrowing_result_all/contribution_enhancements")
TABLE_PATH <- file.path(OUTPUT_PATH, "tables")
FIG_PATH <- file.path(OUTPUT_PATH, "figures")
for (path in c(TABLE_PATH, FIG_PATH)) {
if (!dir.exists(path)) dir.create(path, recursive = TRUE)
}
# Key dates
ACUTE_START <- as.Date("2023-03-13")
ACUTE_END <- as.Date("2023-05-01")
DATE_BTFP_ANNOUNCE <- as.Date("2023-03-12")
BASELINE_MAIN <- "2022Q4"
cat("Paths configured:\n")
## Paths configured:
cat(" Tables:", TABLE_PATH, "\n")
## Tables: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/crisis_borrowing_result_all/contribution_enhancements/tables
cat(" Figures:", FIG_PATH, "\n")
## Figures: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/crisis_borrowing_result_all/contribution_enhancements/figures
# ==============================================================================
# HELPER FUNCTIONS (Exactly as in your main analysis)
# ==============================================================================
winsorize <- function(x, probs = c(0.025, 0.975)) {
if (all(is.na(x))) return(x)
q <- quantile(x, probs = probs, na.rm = TRUE, names = FALSE)
pmax(pmin(x, q[2]), q[1])
}
standardize_z <- function(x) {
if (all(is.na(x))) return(x)
(x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}
safe_div <- function(num, denom, default = NA_real_) {
ifelse(is.na(denom) | denom == 0, default, num / denom)
}
# ==============================================================================
# SAVE FUNCTIONS - NOW PROPERLY SAVING TABLES
# ==============================================================================
save_reg_table <- function(models, filename, title_text = "", notes_text = "",
coef_map_use = NULL, gof_map_use = NULL, add_rows_use = NULL, ...) {
# HTML
modelsummary(models, stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = coef_map_use, gof_map = gof_map_use, add_rows = add_rows_use,
title = title_text, notes = notes_text,
output = file.path(TABLE_PATH, paste0(filename, ".html")), ...)
# LaTeX
modelsummary(models, stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = coef_map_use, gof_map = gof_map_use, add_rows = add_rows_use,
title = title_text, notes = notes_text,
output = file.path(TABLE_PATH, paste0(filename, ".tex")), ...)
cat("✓ Saved table:", filename, "(HTML + LaTeX)\n")
}
# Save kable table (for summary tables)
save_kable_table <- function(data, filename, caption_text = "", notes_text = "") {
# HTML version
html_tbl <- data %>%
kable(format = "html", caption = caption_text) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, font_size = 11)
save_kable(html_tbl, file = file.path(TABLE_PATH, paste0(filename, ".html")))
# LaTeX version
latex_tbl <- data %>%
kable(format = "latex", caption = caption_text, booktabs = TRUE) %>%
kable_styling(latex_options = c("hold_position", "scale_down"))
if (notes_text != "") {
latex_tbl <- latex_tbl %>%
footnote(general = notes_text, general_title = "Notes: ", threeparttable = TRUE)
}
writeLines(latex_tbl, file.path(TABLE_PATH, paste0(filename, ".tex")))
cat("✓ Saved table:", filename, "(HTML + LaTeX)\n")
}
save_figure <- function(plot_obj, filename, width = 12, height = 8) {
ggsave(file.path(FIG_PATH, paste0(filename, ".pdf")), plot = plot_obj,
width = width, height = height, device = "pdf")
ggsave(file.path(FIG_PATH, paste0(filename, ".png")), plot = plot_obj,
width = width, height = height, dpi = 300, device = "png")
cat("✓ Saved figure:", filename, "(PDF + PNG)\n")
}
# ==============================================================================
# LOAD DATA (Same as your main analysis)
# ==============================================================================
call_q <- read_csv(file.path(DATA_PROC, "final_call_gsib.csv"), show_col_types = FALSE) %>%
mutate(idrssd = as.character(idrssd))
btfp_loans_raw <- read_csv(file.path(DATA_PROC, "btfp_loan_bank_only.csv"), show_col_types = FALSE) %>%
mutate(rssd_id = as.character(rssd_id), btfp_loan_date = mdy(btfp_loan_date))
dw_loans_raw <- read_csv(file.path(DATA_PROC, "dw_loan_bank_2023.csv"), show_col_types = FALSE) %>%
mutate(rssd_id = as.character(rssd_id), dw_loan_date = ymd(dw_loan_date))
cat("Data loaded successfully.\n")
## Data loaded successfully.
From Goldstein & Pauzner (2005), a bank run occurs when the fundamental θ falls below the run threshold θ*:
\[\theta^* = f(\text{Fragility}, \text{Coordination Risk})\]
Key insight: We observe borrowing as revealed run risk. Banks borrow when θ < θ*.
# ==============================================================================
# PILLAR 1: RUN THRESHOLD ESTIMATION
# ==============================================================================
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
##
## ================================================================================
cat("PILLAR 1: STRUCTURAL ESTIMATION OF RUN THRESHOLDS\n")
## PILLAR 1: STRUCTURAL ESTIMATION OF RUN THRESHOLDS
cat(paste(rep("=", 80), collapse = ""), "\n")
## ================================================================================
# Prepare acute period data (same exclusions as your main analysis)
excluded_banks <- c("99920", "91989", "852218", "3212149") # SVB, Signature, First Republic, Silvergate
df_baseline <- call_q %>%
filter(period == BASELINE_MAIN, !idrssd %in% excluded_banks) %>%
mutate(
# Core variables (winsorized then z-scored)
tier1_ratio_raw = tier1cap_to_total_asset,
roa_raw = roa,
fhlb_ratio_raw = fhlb_to_total_asset,
mtm_total_w = winsorize(mtm_loss_to_total_asset),
uninsured_lev_w = winsorize(uninsured_deposit_to_total_asset),
ln_assets_w = winsorize(log(total_asset)),
cash_ratio_w = winsorize(cash_to_total_asset),
loan_to_deposit_w = winsorize(loan_to_deposit),
book_equity_w = winsorize(book_equity_to_total_asset),
wholesale_raw = safe_div(
fed_fund_purchase + repo + replace_na(other_borrowed_less_than_1yr, 0),
total_liability, 0
) * 100,
wholesale_w = winsorize(wholesale_raw),
tier1_ratio_w = winsorize(tier1_ratio_raw),
roa_w = winsorize(roa_raw),
fhlb_ratio_w = winsorize(fhlb_ratio_raw),
# Z-standardization
mtm_total = standardize_z(mtm_total_w),
uninsured_lev = standardize_z(uninsured_lev_w),
ln_assets = standardize_z(ln_assets_w),
cash_ratio = standardize_z(cash_ratio_w),
loan_to_deposit = standardize_z(loan_to_deposit_w),
book_equity_ratio = standardize_z(book_equity_w),
wholesale = standardize_z(wholesale_w),
tier1_ratio = standardize_z(tier1_ratio_w),
roa = standardize_z(roa_w),
fhlb_ratio = standardize_z(fhlb_ratio_w),
loan_to_deposit = standardize_z(loan_to_deposit_w),
# Interaction
mtm_x_uninsured = mtm_total * uninsured_lev
)
# Add borrowing indicators
btfp_acute <- btfp_loans_raw %>%
filter(btfp_loan_date >= ACUTE_START, btfp_loan_date <= ACUTE_END) %>%
distinct(rssd_id) %>%
mutate(btfp_acute = 1)
df_acute <- df_baseline %>%
left_join(btfp_acute, by = c("idrssd" = "rssd_id")) %>%
mutate(btfp_acute = replace_na(btfp_acute, 0))
# ==============================================================================
# STEP 1: Estimate Probit Model (Latent Threshold Structure)
# ==============================================================================
CONTROLS <- "ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio + roa + wholesale"
f_probit <- as.formula(paste("btfp_acute ~ mtm_total * uninsured_lev +", CONTROLS))
# Filter to complete cases to avoid prediction issues
vars_needed <- c("btfp_acute", "mtm_total", "uninsured_lev", "ln_assets",
"cash_ratio", "loan_to_deposit", "book_equity_ratio",
"roa", "wholesale")
df_acute_complete <- df_acute %>%
filter(complete.cases(across(all_of(vars_needed))))
model_probit <- glm(f_probit,
data = df_acute_complete,
family = binomial(link = "probit"))
cat("\n=== PROBIT MODEL FOR THRESHOLD ESTIMATION ===\n")
##
## === PROBIT MODEL FOR THRESHOLD ESTIMATION ===
cat("Sample size:", nrow(df_acute_complete), "\n")
## Sample size: 4717
cat("BTFP borrowers:", sum(df_acute_complete$btfp_acute), "\n")
## BTFP borrowers: 484
# ==============================================================================
# STEP 2: Recover Bank-Specific Run Thresholds
# ==============================================================================
df_acute_complete <- df_acute_complete %>%
mutate(
linear_index = predict(model_probit, type = "link"),
prob_borrow = predict(model_probit, type = "response"),
theta_star = -linear_index
)
beta_mtm <- coef(model_probit)["mtm_total"]
df_acute_complete <- df_acute_complete %>%
mutate(threshold_mtm_pct = theta_star / abs(beta_mtm))
cat("\n=== RUN THRESHOLD DISTRIBUTION ===\n")
##
## === RUN THRESHOLD DISTRIBUTION ===
cat("Mean θ*:", round(mean(df_acute_complete$theta_star, na.rm = TRUE), 3), "\n")
## Mean θ*: 1.458
cat("SD θ*:", round(sd(df_acute_complete$theta_star, na.rm = TRUE), 3), "\n")
## SD θ*: 0.604
# ==============================================================================
# STEP 3: KEY OUTPUT - Threshold by Fragility Tercile
# ==============================================================================
threshold_by_fragility <- df_acute_complete %>%
mutate(fragility_tercile = ntile(uninsured_lev, 3)) %>%
group_by(fragility_tercile) %>%
summarise(
n_banks = n(),
mean_uninsured_raw = mean(uninsured_lev_w, na.rm = TRUE),
mean_theta_star = mean(theta_star, na.rm = TRUE),
sd_theta_star = sd(theta_star, na.rm = TRUE),
se_theta_star = sd_theta_star / sqrt(n_banks),
mean_borrow_rate = mean(btfp_acute, na.rm = TRUE) * 100,
.groups = "drop"
) %>%
mutate(
fragility_label = case_when(
fragility_tercile == 1 ~ "Low Fragility",
fragility_tercile == 2 ~ "Medium Fragility",
fragility_tercile == 3 ~ "High Fragility"
),
threshold_shift = mean_theta_star - first(mean_theta_star)
)
cat("\n=== RUN THRESHOLD BY FUNDING FRAGILITY ===\n")
##
## === RUN THRESHOLD BY FUNDING FRAGILITY ===
print(threshold_by_fragility)
## # A tibble: 3 × 9
## fragility_tercile n_banks mean_uninsured_raw mean_theta_star sd_theta_star
## <int> <int> <dbl> <dbl> <dbl>
## 1 1 1573 11.0 1.79 0.642
## 2 2 1572 22.0 1.38 0.458
## 3 3 1572 36.5 1.20 0.540
## # ℹ 4 more variables: se_theta_star <dbl>, mean_borrow_rate <dbl>,
## # fragility_label <chr>, threshold_shift <dbl>
# Calculate key statistic
threshold_diff <- threshold_by_fragility %>%
filter(fragility_tercile %in% c(1, 3)) %>%
summarise(diff = diff(mean_theta_star)) %>%
pull(diff)
cat("\n=== KEY CONTRIBUTION STATISTIC ===\n")
##
## === KEY CONTRIBUTION STATISTIC ===
cat("Threshold difference (High vs Low Fragility):", round(threshold_diff, 3), "SD\n")
## Threshold difference (High vs Low Fragility): -0.588 SD
# ==============================================================================
# SAVE TABLE: Run Threshold by Fragility
# ==============================================================================
table_threshold <- threshold_by_fragility %>%
select(
`Fragility Tercile` = fragility_label,
`N Banks` = n_banks,
`Mean Uninsured Deposit Ratio` = mean_uninsured_raw,
`Mean θ*` = mean_theta_star,
`SE(θ*)` = se_theta_star,
`BTFP Usage (%)` = mean_borrow_rate,
`Threshold Shift` = threshold_shift
) %>%
mutate(across(where(is.numeric), ~round(., 3)))
save_kable_table(
table_threshold,
"Table_Run_Threshold_by_Fragility",
caption_text = "Table: Estimated Run Thresholds by Funding Fragility",
notes_text = "θ* estimated from probit model of BTFP usage. Higher θ* indicates lower run threshold (more vulnerable). Threshold Shift shows difference relative to Low Fragility tercile."
)
## ✓ Saved table: Table_Run_Threshold_by_Fragility (HTML + LaTeX)
# Also save as CSV for easy access
write_csv(table_threshold, file.path(TABLE_PATH, "Table_Run_Threshold_by_Fragility.csv"))
cat("✓ Also saved CSV version\n")
## ✓ Also saved CSV version
# ==============================================================================
# FIGURE: RUN THRESHOLD DISTRIBUTION BY FRAGILITY
# ==============================================================================
p_threshold <- ggplot(df_acute_complete, aes(x = theta_star, fill = factor(ntile(uninsured_lev, 3)))) +
geom_density(alpha = 0.5) +
geom_vline(xintercept = 0, linetype = "dashed", color = "gray40") +
scale_fill_manual(
values = c("1" = "#4575B4", "2" = "#FFFFBF", "3" = "#D73027"),
labels = c("Low Fragility", "Medium Fragility", "High Fragility"),
name = "Funding Fragility\n(Uninsured Deposit Tercile)"
) +
labs(
title = "Figure: Estimated Run Thresholds by Funding Fragility",
subtitle = "High-fragility banks have higher θ* (run at better fundamentals)",
x = expression("Estimated Run Threshold ("*theta*"*)"),
y = "Density",
caption = paste0("Note: θ* estimated from probit model. Difference between high and low fragility: ",
round(abs(threshold_diff), 2), " SD")
) +
theme_minimal(base_size = 14) +
theme(legend.position = "bottom",
plot.title = element_text(face = "bold"))
print(p_threshold)
save_figure(p_threshold, "Figure_Run_Threshold_Distribution", width = 12, height = 8)
## ✓ Saved figure: Figure_Run_Threshold_Distribution (PDF + PNG)
# Bar chart of mean thresholds
p_marginal <- ggplot(threshold_by_fragility, aes(x = fragility_label, y = mean_theta_star)) +
geom_col(fill = "#2166AC", alpha = 0.8, width = 0.6) +
geom_errorbar(aes(ymin = mean_theta_star - 1.96*se_theta_star,
ymax = mean_theta_star + 1.96*se_theta_star),
width = 0.2) +
geom_hline(yintercept = 0, linetype = "dashed") +
labs(
title = "Figure: Mean Run Threshold by Funding Fragility",
subtitle = "Higher θ* = more vulnerable to runs at same fundamentals",
x = "Funding Fragility Category",
y = expression("Mean Run Threshold ("*theta*"*)"),
caption = "Error bars: 95% CI"
) +
theme_minimal(base_size = 14) +
theme(plot.title = element_text(face = "bold"))
print(p_marginal)
save_figure(p_marginal, "Figure_Threshold_by_Fragility", width = 10, height = 6)
## ✓ Saved figure: Figure_Threshold_by_Fragility (PDF + PNG)
Conventional wisdom: “BTFP was an implicit subsidy/bailout because of par-value lending”
Your finding: “BTFP’s par-value feature actually SCREENED for genuine run risk”
# ==============================================================================
# PILLAR 2: BTFP SCREENING MECHANISM TEST
# ==============================================================================
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
##
## ================================================================================
cat("PILLAR 2: BTFP AS SCREENING MECHANISM\n")
## PILLAR 2: BTFP AS SCREENING MECHANISM
cat(paste(rep("=", 80), collapse = ""), "\n")
## ================================================================================
# Create risk categories
df_acute_complete <- df_acute_complete %>%
mutate(
mtm_high = mtm_total > 0,
uninsured_high = uninsured_lev > 0,
run_risk_cat = case_when(
!mtm_high & !uninsured_high ~ "R1: Low MTM, Low Unins",
!mtm_high & uninsured_high ~ "R2: Low MTM, High Unins",
mtm_high & !uninsured_high ~ "R3: High MTM, Low Unins",
mtm_high & uninsured_high ~ "R4: High MTM, High Unins"
),
run_risk_1 = as.integer(run_risk_cat == "R1: Low MTM, Low Unins"),
run_risk_2 = as.integer(run_risk_cat == "R2: Low MTM, High Unins"),
run_risk_3 = as.integer(run_risk_cat == "R3: High MTM, Low Unins"),
run_risk_4 = as.integer(run_risk_cat == "R4: High MTM, High Unins")
)
# ==============================================================================
# TEST 1: Facility Choice by Risk Category
# ==============================================================================
facility_by_risk <- df_acute_complete %>%
group_by(run_risk_cat) %>%
summarise(
n_banks = n(),
n_btfp = sum(btfp_acute),
pct_btfp = 100 * mean(btfp_acute, na.rm = TRUE),
avg_mtm = mean(mtm_total_w, na.rm = TRUE),
avg_uninsured = mean(uninsured_lev_w, na.rm = TRUE),
.groups = "drop"
)
cat("\n=== BTFP USAGE BY RISK CATEGORY ===\n")
##
## === BTFP USAGE BY RISK CATEGORY ===
print(facility_by_risk)
## # A tibble: 4 × 6
## run_risk_cat n_banks n_btfp pct_btfp avg_mtm avg_uninsured
## <chr> <int> <dbl> <dbl> <dbl> <dbl>
## 1 R1: Low MTM, Low Unins 1255 61 4.86 3.64 14.2
## 2 R2: Low MTM, High Unins 1201 121 10.1 3.67 34.3
## 3 R3: High MTM, Low Unins 1299 124 9.55 7.45 14.9
## 4 R4: High MTM, High Unins 962 178 18.5 7.03 32.0
# Key comparison: Risk 3 vs Risk 4
r3_rate <- facility_by_risk %>% filter(run_risk_cat == "R3: High MTM, Low Unins") %>% pull(pct_btfp)
r4_rate <- facility_by_risk %>% filter(run_risk_cat == "R4: High MTM, High Unins") %>% pull(pct_btfp)
cat("\n=== KEY SCREENING TEST ===\n")
##
## === KEY SCREENING TEST ===
cat("Risk 3 (High MTM, Low Uninsured) BTFP rate:", round(r3_rate, 1), "%\n")
## Risk 3 (High MTM, Low Uninsured) BTFP rate: 9.5 %
cat("Risk 4 (High MTM, High Uninsured) BTFP rate:", round(r4_rate, 1), "%\n")
## Risk 4 (High MTM, High Uninsured) BTFP rate: 18.5 %
cat("Difference (R4 - R3):", round(r4_rate - r3_rate, 1), "pp\n")
## Difference (R4 - R3): 9 pp
# ==============================================================================
# SAVE TABLE: Facility Usage by Risk Category
# ==============================================================================
table_risk_usage <- facility_by_risk %>%
select(
`Risk Category` = run_risk_cat,
`N Banks` = n_banks,
`N BTFP Users` = n_btfp,
`BTFP Usage (%)` = pct_btfp,
`Mean MTM Loss` = avg_mtm,
`Mean Uninsured Ratio` = avg_uninsured
) %>%
mutate(across(where(is.numeric), ~round(., 2)))
save_kable_table(
table_risk_usage,
"Table_BTFP_Usage_by_Risk_Category",
caption_text = "Table: BTFP Usage by Risk Category",
notes_text = "Risk categories based on median splits of MTM losses and uninsured deposit ratio. R4-R3 difference tests whether BTFP screened for run risk vs. collateral value."
)
## ✓ Saved table: Table_BTFP_Usage_by_Risk_Category (HTML + LaTeX)
write_csv(table_risk_usage, file.path(TABLE_PATH, "Table_BTFP_Usage_by_Risk_Category.csv"))
# ==============================================================================
# TEST 2: Regression Evidence for Screening
# ==============================================================================
m_mtm_only <- feols(btfp_acute ~ mtm_total + ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio,
data = df_acute_complete, vcov = "hetero")
m_both <- feols(btfp_acute ~ mtm_total + uninsured_lev + ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio,
data = df_acute_complete, vcov = "hetero")
m_interaction <- feols(btfp_acute ~ mtm_total * uninsured_lev + ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio,
data = df_acute_complete, vcov = "hetero")
m_risk <- feols(btfp_acute ~ run_risk_2 + run_risk_3 + run_risk_4 + ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio,
data = df_acute_complete, vcov = "hetero")
screening_models <- list(
"(1) MTM Only" = m_mtm_only,
"(2) MTM + Uninsured" = m_both,
"(3) With Interaction" = m_interaction,
"(4) Risk Categories" = m_risk
)
COEF_MAP_SCREEN <- c(
"mtm_total" = "MTM Loss (z)",
"uninsured_lev" = "Uninsured Leverage (z)",
"mtm_total:uninsured_lev" = "MTM × Uninsured",
"run_risk_2" = "Risk 2: Low MTM, High Unins",
"run_risk_3" = "Risk 3: High MTM, Low Unins",
"run_risk_4" = "Risk 4: High MTM, High Unins",
"ln_assets" = "Log(Assets)",
"cash_ratio" = "Cash Ratio",
"loan_to_deposit" = "Loan/Deposit",
"book_equity_ratio" = "Book Equity Ratio",
"roa" = "ROA",
"wholesale" = "Wholesale Funding"
)
# Display table
modelsummary(
screening_models,
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = COEF_MAP_SCREEN,
gof_map = c("nobs", "r.squared"),
title = "Table: BTFP Screening Mechanism Test"
)
| (1) MTM Only | (2) MTM + Uninsured | (3) With Interaction | (4) Risk Categories | |
|---|---|---|---|---|
| * p < 0.1, ** p < 0.05, *** p < 0.01 | ||||
| MTM Loss (z) | 0.016*** | 0.017*** | 0.021*** | |
| (0.004) | (0.004) | (0.005) | ||
| Uninsured Leverage (z) | 0.010* | 0.014** | ||
| (0.005) | (0.006) | |||
| MTM × Uninsured | 0.015*** | |||
| (0.004) | ||||
| Risk 2: Low MTM, High Unins | -0.001 | |||
| (0.011) | ||||
| Risk 3: High MTM, Low Unins | 0.009 | |||
| (0.010) | ||||
| Risk 4: High MTM, High Unins | 0.058*** | |||
| (0.015) | ||||
| Log(Assets) | 0.058*** | 0.053*** | 0.052*** | 0.053*** |
| (0.005) | (0.006) | (0.006) | (0.006) | |
| Cash Ratio | -0.023*** | -0.023*** | -0.021*** | -0.024*** |
| (0.004) | (0.004) | (0.004) | (0.004) | |
| Loan/Deposit | -0.018*** | -0.017*** | -0.013*** | -0.015*** |
| (0.004) | (0.004) | (0.004) | (0.004) | |
| Book Equity Ratio | -0.016*** | -0.013*** | -0.013*** | -0.015*** |
| (0.003) | (0.003) | (0.003) | (0.003) | |
| Num.Obs. | 4717 | 4717 | 4717 | 4717 |
| R2 | 0.059 | 0.060 | 0.062 | 0.062 |
# ==============================================================================
# SAVE TABLE: Screening Mechanism Regression
# ==============================================================================
save_reg_table(
screening_models,
"Table_BTFP_Screening_Mechanism",
title_text = "Table: BTFP Screening Mechanism Test",
notes_text = "LPM estimates. If BTFP were purely collateral-driven, Risk 3 coefficient should equal Risk 4. Significant interaction and R4>R3 indicate BTFP screened for run risk. Heteroskedasticity-robust SE. *p<0.10, **p<0.05, ***p<0.01.",
coef_map_use = COEF_MAP_SCREEN,
gof_map_use = c("nobs", "r.squared")
)
## ✓ Saved table: Table_BTFP_Screening_Mechanism (HTML + LaTeX)
# Key statistics
interaction_coef <- coef(m_interaction)["mtm_total:uninsured_lev"]
interaction_se <- sqrt(vcov(m_interaction)["mtm_total:uninsured_lev", "mtm_total:uninsured_lev"])
cat("\n=== INTERACTION COEFFICIENT ===\n")
##
## === INTERACTION COEFFICIENT ===
cat("β(MTM × Uninsured):", round(interaction_coef, 4), "\n")
## β(MTM × Uninsured): 0.0146
cat("SE:", round(interaction_se, 4), "\n")
## SE: 0.0041
cat("t-stat:", round(interaction_coef/interaction_se, 2), "\n")
## t-stat: 3.6
# ==============================================================================
# PILLAR 2B: DW COMPARISON (Critical Robustness)
# ==============================================================================
# Add DW indicator for acute period
dw_acute_ind <- dw_loans_raw %>%
filter(dw_loan_date >= ACUTE_START, dw_loan_date <= ACUTE_END) %>%
distinct(rssd_id) %>%
mutate(dw_acute = 1)
df_acute_complete <- df_acute_complete %>%
left_join(dw_acute_ind, by = c("idrssd" = "rssd_id")) %>%
mutate(dw_acute = replace_na(dw_acute, 0))
# Run same models for DW
m_dw_interaction <- feols(dw_acute ~ mtm_total * uninsured_lev + ln_assets + cash_ratio +
loan_to_deposit + book_equity_ratio + roa + wholesale,
data = df_acute_complete, vcov = "hetero")
m_dw_risk <- feols(dw_acute ~ run_risk_2 + run_risk_3 + run_risk_4 + ln_assets + cash_ratio +
loan_to_deposit + book_equity_ratio + roa + wholesale,
data = df_acute_complete, vcov = "hetero")
# Compare BTFP vs DW side-by-side
comparison_models <- list(
"BTFP: Interaction" = m_interaction,
"BTFP: Risk Cat" = m_risk,
"DW: Interaction" = m_dw_interaction,
"DW: Risk Cat" = m_dw_risk
)
save_reg_table(
comparison_models,
"Table_BTFP_vs_DW_Run_Dynamics",
title_text = "Table: Run Dynamics Test — BTFP vs. DW Comparison",
notes_text = "Key test: If interaction is significant for BOTH facilities, pattern reflects run dynamics (not BTFP's par-value feature). DW valued collateral at market prices with haircuts, yet shows same pattern.",
coef_map_use = COEF_MAP_SCREEN,
gof_map_use = c("nobs", "r.squared")
)
## ✓ Saved table: Table_BTFP_vs_DW_Run_Dynamics (HTML + LaTeX)
# ==============================================================================
# FIGURE: BTFP AS SCREENING MECHANISM
# ==============================================================================
p_screen <- facility_by_risk %>%
ggplot(aes(x = reorder(run_risk_cat, pct_btfp), y = pct_btfp, fill = run_risk_cat)) +
geom_col(alpha = 0.8, width = 0.7) +
geom_text(aes(label = sprintf("%.1f%%", pct_btfp)), hjust = -0.1, size = 4) +
coord_flip() +
scale_fill_manual(values = c(
"R1: Low MTM, Low Unins" = "#4575B4",
"R2: Low MTM, High Unins" = "#91BFDB",
"R3: High MTM, Low Unins" = "#FC8D59",
"R4: High MTM, High Unins" = "#D73027"
)) +
scale_y_continuous(limits = c(0, max(facility_by_risk$pct_btfp) * 1.2)) +
labs(
title = "Figure: BTFP Usage by Risk Category — Evidence of Screening",
subtitle = "If BTFP were purely collateral-driven, Risk 3 would equal Risk 4",
x = NULL,
y = "BTFP Usage Rate (%)",
caption = paste0("Risk 4 - Risk 3 difference: ", round(r4_rate - r3_rate, 1),
" pp. This gap shows BTFP screened for run risk.")
) +
theme_minimal(base_size = 14) +
theme(legend.position = "none",
plot.title = element_text(face = "bold"))
print(p_screen)
save_figure(p_screen, "Figure_BTFP_Screening_Mechanism", width = 12, height = 6)
## ✓ Saved figure: Figure_BTFP_Screening_Mechanism (PDF + PNG)
# ==============================================================================
# PILLAR 3: WELFARE COUNTERFACTUAL
# ==============================================================================
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
##
## ================================================================================
cat("PILLAR 3: WELFARE COUNTERFACTUAL\n")
## PILLAR 3: WELFARE COUNTERFACTUAL
cat(paste(rep("=", 80), collapse = ""), "\n")
## ================================================================================
# Add DW indicator for pre-BTFP period
dw_prebtfp <- dw_loans_raw %>%
filter(dw_loan_date >= as.Date("2023-03-01"), dw_loan_date < DATE_BTFP_ANNOUNCE) %>%
distinct(rssd_id) %>%
mutate(dw_prebtfp = 1)
df_acute_complete <- df_acute_complete %>%
left_join(dw_prebtfp, by = c("idrssd" = "rssd_id")) %>%
mutate(dw_prebtfp = replace_na(dw_prebtfp, 0))
# Pre-BTFP model
m_prebtfp <- glm(dw_prebtfp ~ mtm_total * uninsured_lev + ln_assets + cash_ratio +
loan_to_deposit + book_equity_ratio,
data = df_acute_complete, family = binomial(link = "probit"))
# Calculate counterfactual probabilities
df_acute_complete <- df_acute_complete %>%
mutate(
prob_run_no_btfp = predict(m_prebtfp, newdata = ., type = "response"),
at_risk = prob_run_no_btfp > 0.10
)
# ==============================================================================
# Welfare Summary
# ==============================================================================
welfare_summary <- df_acute_complete %>%
summarise(
total_banks = n(),
banks_at_risk = sum(at_risk, na.rm = TRUE),
pct_at_risk = 100 * mean(at_risk, na.rm = TRUE),
mean_prob_no_btfp = mean(prob_run_no_btfp, na.rm = TRUE),
total_assets_bn = sum(total_asset, na.rm = TRUE) / 1e9,
assets_at_risk_bn = sum(total_asset[at_risk], na.rm = TRUE) / 1e9,
banks_stabilized = sum(at_risk & btfp_acute == 0, na.rm = TRUE)
)
cat("\n=== COUNTERFACTUAL WELFARE ANALYSIS ===\n")
##
## === COUNTERFACTUAL WELFARE ANALYSIS ===
cat("Total banks:", welfare_summary$total_banks, "\n")
## Total banks: 4717
cat("Banks at elevated run risk (>10%):", welfare_summary$banks_at_risk,
"(", round(welfare_summary$pct_at_risk, 1), "%)\n")
## Banks at elevated run risk (>10%): 176 ( 3.7 %)
cat("Total assets: $", round(welfare_summary$total_assets_bn, 0), " billion\n")
## Total assets: $ 20 billion
cat("Assets at risk: $", round(welfare_summary$assets_at_risk_bn, 0), " billion\n")
## Assets at risk: $ 12 billion
# ==============================================================================
# SAVE TABLE: Counterfactual Welfare
# ==============================================================================
# By risk category
welfare_by_risk <- df_acute_complete %>%
group_by(run_risk_cat) %>%
summarise(
n_banks = n(),
n_at_risk = sum(at_risk, na.rm = TRUE),
pct_at_risk = 100 * mean(at_risk, na.rm = TRUE),
mean_prob = mean(prob_run_no_btfp, na.rm = TRUE),
assets_bn = sum(total_asset, na.rm = TRUE) / 1e9,
assets_at_risk_bn = sum(total_asset[at_risk], na.rm = TRUE) / 1e9,
.groups = "drop"
)
table_welfare <- welfare_by_risk %>%
select(
`Risk Category` = run_risk_cat,
`N Banks` = n_banks,
`N At Risk` = n_at_risk,
`% At Risk` = pct_at_risk,
`Mean Run Prob` = mean_prob,
`Total Assets ($B)` = assets_bn,
`Assets At Risk ($B)` = assets_at_risk_bn
) %>%
mutate(across(where(is.numeric), ~round(., 2)))
# Add total row
total_row <- tibble(
`Risk Category` = "TOTAL",
`N Banks` = welfare_summary$total_banks,
`N At Risk` = welfare_summary$banks_at_risk,
`% At Risk` = round(welfare_summary$pct_at_risk, 2),
`Mean Run Prob` = round(welfare_summary$mean_prob_no_btfp, 2),
`Total Assets ($B)` = round(welfare_summary$total_assets_bn, 2),
`Assets At Risk ($B)` = round(welfare_summary$assets_at_risk_bn, 2)
)
table_welfare <- bind_rows(table_welfare, total_row)
save_kable_table(
table_welfare,
"Table_Counterfactual_Welfare",
caption_text = "Table: Counterfactual Welfare Analysis — What if BTFP Hadn't Existed?",
notes_text = "Run probability estimated from pre-BTFP DW borrowing patterns. Banks 'at risk' defined as >10% predicted run probability without BTFP."
)
## ✓ Saved table: Table_Counterfactual_Welfare (HTML + LaTeX)
write_csv(table_welfare, file.path(TABLE_PATH, "Table_Counterfactual_Welfare.csv"))
# ==============================================================================
# FIGURE: COUNTERFACTUAL ANALYSIS
# ==============================================================================
p_counterfactual <- df_acute_complete %>%
ggplot(aes(x = prob_run_no_btfp, y = prob_borrow, color = run_risk_cat)) +
geom_point(alpha = 0.4, size = 2) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray40") +
scale_color_manual(values = c(
"R1: Low MTM, Low Unins" = "#4575B4",
"R2: Low MTM, High Unins" = "#91BFDB",
"R3: High MTM, Low Unins" = "#FC8D59",
"R4: High MTM, High Unins" = "#D73027"
)) +
labs(
title = "Figure: BTFP Stabilization Effect — Counterfactual Analysis",
subtitle = "Points below 45° line = run probability reduced by BTFP availability",
x = "Predicted Run Probability WITHOUT BTFP",
y = "Actual Borrow Probability WITH BTFP",
color = "Risk Category",
caption = paste0("Note: ", welfare_summary$banks_at_risk, " banks (",
round(welfare_summary$pct_at_risk, 1), "%) with >10% run probability without BTFP")
) +
theme_minimal(base_size = 14) +
theme(legend.position = "bottom",
plot.title = element_text(face = "bold"))
print(p_counterfactual)
save_figure(p_counterfactual, "Figure_BTFP_Counterfactual", width = 12, height = 8)
## ✓ Saved figure: Figure_BTFP_Counterfactual (PDF + PNG)
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
##
## ================================================================================
cat("SUMMARY OF ALL SAVED OUTPUTS\n")
## SUMMARY OF ALL SAVED OUTPUTS
cat(paste(rep("=", 80), collapse = ""), "\n")
## ================================================================================
cat("\n=== TABLES SAVED ===\n")
##
## === TABLES SAVED ===
cat("1. Table_Run_Threshold_by_Fragility (.html, .tex, .csv)\n")
## 1. Table_Run_Threshold_by_Fragility (.html, .tex, .csv)
cat("2. Table_BTFP_Usage_by_Risk_Category (.html, .tex, .csv)\n")
## 2. Table_BTFP_Usage_by_Risk_Category (.html, .tex, .csv)
cat("3. Table_BTFP_Screening_Mechanism (.html, .tex)\n")
## 3. Table_BTFP_Screening_Mechanism (.html, .tex)
cat("4. Table_Counterfactual_Welfare (.html, .tex, .csv)\n")
## 4. Table_Counterfactual_Welfare (.html, .tex, .csv)
cat("\n=== FIGURES SAVED ===\n")
##
## === FIGURES SAVED ===
cat("1. Figure_Run_Threshold_Distribution (.pdf, .png)\n")
## 1. Figure_Run_Threshold_Distribution (.pdf, .png)
cat("2. Figure_Threshold_by_Fragility (.pdf, .png)\n")
## 2. Figure_Threshold_by_Fragility (.pdf, .png)
cat("3. Figure_BTFP_Screening_Mechanism (.pdf, .png)\n")
## 3. Figure_BTFP_Screening_Mechanism (.pdf, .png)
cat("4. Figure_BTFP_Counterfactual (.pdf, .png)\n")
## 4. Figure_BTFP_Counterfactual (.pdf, .png)
cat("\n=== OUTPUT LOCATIONS ===\n")
##
## === OUTPUT LOCATIONS ===
cat("Tables:", TABLE_PATH, "\n")
## Tables: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/crisis_borrowing_result_all/contribution_enhancements/tables
cat("Figures:", FIG_PATH, "\n")
## Figures: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/crisis_borrowing_result_all/contribution_enhancements/figures
# List actual files
cat("\n=== FILES IN TABLE DIRECTORY ===\n")
##
## === FILES IN TABLE DIRECTORY ===
if (dir.exists(TABLE_PATH)) {
print(list.files(TABLE_PATH))
} else {
cat("Directory not yet created (will be created when code runs)\n")
}
## [1] "Table_BTFP_Screening_Mechanism.html"
## [2] "Table_BTFP_Screening_Mechanism.tex"
## [3] "Table_BTFP_Usage_by_Risk_Category.csv"
## [4] "Table_BTFP_Usage_by_Risk_Category.html"
## [5] "Table_BTFP_Usage_by_Risk_Category.tex"
## [6] "Table_BTFP_vs_DW_Run_Dynamics.html"
## [7] "Table_BTFP_vs_DW_Run_Dynamics.tex"
## [8] "Table_Counterfactual_Welfare.csv"
## [9] "Table_Counterfactual_Welfare.html"
## [10] "Table_Counterfactual_Welfare.tex"
## [11] "Table_Run_Threshold_by_Fragility.csv"
## [12] "Table_Run_Threshold_by_Fragility.html"
## [13] "Table_Run_Threshold_by_Fragility.tex"
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
##
## ================================================================================
cat("KEY CONTRIBUTION STATEMENTS FOR YOUR PAPER\n")
## KEY CONTRIBUTION STATEMENTS FOR YOUR PAPER
cat(paste(rep("=", 80), collapse = ""), "\n")
## ================================================================================
cat("\n")
cat("PILLAR 1 (Structural Parameter):\n")
## PILLAR 1 (Structural Parameter):
cat("'A one-standard-deviation increase in funding fragility lowers the\n")
## 'A one-standard-deviation increase in funding fragility lowers the
cat("run threshold by approximately", round(abs(threshold_diff), 2), "standard deviations.'\n")
## run threshold by approximately 0.59 standard deviations.'
cat("\n")
cat("PILLAR 2 (Screening Mechanism):\n")
## PILLAR 2 (Screening Mechanism):
cat("'Risk 4 banks (high MTM, high uninsured) used BTFP at", round(r4_rate, 1), "%\n")
## 'Risk 4 banks (high MTM, high uninsured) used BTFP at 18.5 %
cat("vs.", round(r3_rate, 1), "% for Risk 3 banks (high MTM, low uninsured).\n")
## vs. 9.5 % for Risk 3 banks (high MTM, low uninsured).
cat("This", round(r4_rate - r3_rate, 1), "pp gap shows BTFP screened for run risk.'\n")
## This 9 pp gap shows BTFP screened for run risk.'
cat("\n")
cat("PILLAR 3 (Welfare Counterfactual):\n")
## PILLAR 3 (Welfare Counterfactual):
cat("'Without BTFP, approximately", welfare_summary$banks_at_risk, "banks holding\n")
## 'Without BTFP, approximately 176 banks holding
cat("$", round(welfare_summary$assets_at_risk_bn, 0), "billion would have faced elevated run risk.'\n")
## $ 12 billion would have faced elevated run risk.'
sessionInfo()
## R version 4.3.1 (2023-06-16 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 11 x64 (build 26100)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: America/Chicago
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] broom_1.0.8 nnet_7.3-19 patchwork_1.3.0 scales_1.3.0
## [5] kableExtra_1.4.0 modelsummary_2.3.0 fixest_0.12.1 data.table_1.17.0
## [9] lubridate_1.9.4 forcats_1.0.0 stringr_1.5.1 dplyr_1.1.4
## [13] purrr_1.0.2 readr_2.1.5 tidyr_1.3.1 tibble_3.2.1
## [17] ggplot2_3.5.1 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 bayestestR_0.17.0 xfun_0.54
## [4] bslib_0.5.1 insight_1.4.3 lattice_0.21-8
## [7] tzdb_0.4.0 numDeriv_2016.8-1.1 vctrs_0.6.4
## [10] tools_4.3.1 generics_0.1.3 datawizard_1.3.0
## [13] parallel_4.3.1 sandwich_3.1-1 fansi_1.0.5
## [16] pkgconfig_2.0.3 tinytable_0.15.1 checkmate_2.3.1
## [19] stringmagic_1.1.2 lifecycle_1.0.4 compiler_4.3.1
## [22] farver_2.1.1 textshaping_0.3.7 munsell_0.5.0
## [25] litedown_0.7 htmltools_0.5.9 sass_0.4.9
## [28] yaml_2.3.7 Formula_1.2-5 crayon_1.5.3
## [31] pillar_1.9.0 jquerylib_0.1.4 cachem_1.0.8
## [34] nlme_3.1-162 tidyselect_1.2.1 digest_0.6.33
## [37] performance_0.15.3 stringi_1.7.12 labeling_0.4.3
## [40] fastmap_1.1.1 grid_4.3.1 colorspace_2.1-0
## [43] cli_3.6.5 magrittr_2.0.3 utf8_1.2.4
## [46] withr_2.5.2 dreamerr_1.4.0 backports_1.4.1
## [49] bit64_4.0.5 timechange_0.3.0 rmarkdown_2.29
## [52] bit_4.0.5 ragg_1.3.0 zoo_1.8-12
## [55] hms_1.1.3 evaluate_0.23 knitr_1.50
## [58] parameters_0.28.3 viridisLite_0.4.2 rlang_1.1.6
## [61] Rcpp_1.0.12 glue_1.8.0 xml2_1.3.6
## [64] svglite_2.1.3 rstudioapi_0.16.0 vroom_1.6.5
## [67] jsonlite_1.8.7 R6_2.5.1 tables_0.9.31
## [70] systemfonts_1.0.6