All packages loaded successfully.
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
# CRITICAL: Winsorization function - applied to ALL continuous variables
winsorize <- function(x, probs = c(0.01, 0.99)) {
if (all(is.na(x))) return(x)
q <- quantile(x, probs = probs, na.rm = TRUE, names = FALSE)
pmax(pmin(x, q[2]), q[1])
}
# Safe division (handles division by zero)
safe_div <- function(num, denom, default = NA_real_) {
ifelse(is.na(denom) | denom == 0, default, num / denom)
}
# Significance stars
add_stars <- function(pval) {
case_when(pval < 0.01 ~ "***", pval < 0.05 ~ "**", pval < 0.10 ~ "*", TRUE ~ "")
}
# Publication theme
theme_pub <- function(base_size = 12) {
theme_minimal(base_size = base_size) +
theme(
plot.title = element_text(face = "bold", size = base_size + 2),
plot.subtitle = element_text(color = "gray40"),
legend.position = "bottom",
panel.grid.minor = element_blank(),
axis.title = element_text(face = "bold")
)
}
# Colors
COLORS <- list(
btfp = "#2E86AB", dw = "#A23B72", both = "#F18F01", neither = "gray70",
acute = "#FC9272", post = "#A1D99B", arb = "#9ECAE1"
)
# Format coefficient with SE
format_coef <- function(est, se, pval) {
sprintf("%.4f%s\n(%.4f)", est, add_stars(pval), se)
}
Data Preparation
# ============================================================================
# LOAD DATA
# ============================================================================
BASE_PATH <- "C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025"
DATA_PROC <- file.path(BASE_PATH, "01_data/processed")
BASELINE_DATE <- "2022Q4"
# Load datasets
call_q <- read_csv(file.path(DATA_PROC, "final_call_gsib.csv"), show_col_types = FALSE) %>%
mutate(idrssd = as.character(idrssd))
btfp_loans <- 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 <- 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 ===\n")
=== DATA LOADED ===
cat("Call Report:", nrow(call_q), "| BTFP Loans:", nrow(btfp_loans), "| DW Loans:", nrow(dw_loans), "\n")
Call Report: 61002 | BTFP Loans: 6734 | DW Loans: 20219
# Assign periods to loans
assign_period <- function(date, periods_df) {
sapply(date, function(d) {
if (is.na(d)) return(NA_integer_)
match <- periods_df %>% filter(d >= start_date & d <= end_date) %>% pull(period_num)
if (length(match) == 0) NA_integer_ else match[1]
})
}
btfp_loans <- btfp_loans %>% mutate(period = assign_period(btfp_loan_date, periods))
dw_loans <- dw_loans %>% mutate(period = assign_period(dw_loan_date, periods))
# Exclude failed banks and G-SIBs
excluded <- call_q %>%
filter(quarter == BASELINE_DATE, failed_bank == 1 | gsib == 1) %>%
pull(idrssd)
btfp_filt <- btfp_loans %>% filter(!rssd_id %in% excluded)
dw_filt <- dw_loans %>% filter(!rssd_id %in% excluded)
cat("Excluded:", length(excluded), "banks (failed + G-SIBs)\n")
Excluded: 41 banks (failed + G-SIBs)
# ============================================================================
# AGGREGATE BORROWER-LEVEL DATA
# ============================================================================
# BTFP borrowers (post-announcement)
btfp_agg <- btfp_filt %>%
filter(!is.na(period), period >= 2) %>%
group_by(rssd_id) %>%
summarise(
btfp = 1L,
btfp_amount = sum(btfp_loan_amount, na.rm = TRUE),
btfp_first_date = min(btfp_loan_date),
btfp_first_period = first(period[btfp_loan_date == min(btfp_loan_date)]),
btfp_n_loans = n(),
btfp_collateral = sum(btfp_total_collateral, na.rm = TRUE),
.groups = "drop"
) %>% rename(idrssd = rssd_id)
# DW post-BTFP
dw_post_agg <- dw_filt %>%
filter(!is.na(period), period >= 2) %>%
group_by(rssd_id) %>%
summarise(
dw = 1L,
dw_amount = sum(dw_loan_amount, na.rm = TRUE),
dw_first_date = min(dw_loan_date),
dw_first_period = first(period[dw_loan_date == min(dw_loan_date)]),
dw_omo_coll = sum(dw_omo_eligible, na.rm = TRUE),
dw_non_omo_coll = sum(dw_non_omo_eligible, na.rm = TRUE),
.groups = "drop"
) %>% rename(idrssd = rssd_id)
# DW pre-BTFP (PLACEBO - revealed preference / operational readiness)
dw_pre_agg <- dw_filt %>%
filter(dw_loan_date < as.Date("2023-03-13")) %>%
group_by(rssd_id) %>%
summarise(
dw_pre = 1L,
dw_pre_amount = sum(dw_loan_amount, na.rm = TRUE),
dw_pre_first_date = min(dw_loan_date),
.groups = "drop"
) %>% rename(idrssd = rssd_id)
cat("BTFP borrowers:", nrow(btfp_agg), "| DW post:", nrow(dw_post_agg), "| DW pre (placebo):", nrow(dw_pre_agg), "\n")
BTFP borrowers: 1316 | DW post: 1077 | DW pre (placebo): 1352
# ============================================================================
# CONSTRUCT ANALYSIS DATASET
# ============================================================================
baseline <- call_q %>%
filter(quarter == BASELINE_DATE, !idrssd %in% excluded)
# Merge and construct RAW variables first
df_raw <- baseline %>%
left_join(btfp_agg, by = "idrssd") %>%
left_join(dw_post_agg, by = "idrssd") %>%
left_join(dw_pre_agg, by = "idrssd") %>%
mutate(
# Fill NAs for binary indicators
btfp = replace_na(btfp, 0L),
dw = replace_na(dw, 0L),
dw_pre = replace_na(dw_pre, 0L),
# ================================================================
# DEPENDENT VARIABLES
# ================================================================
any_fed = as.integer(btfp == 1 | dw == 1),
both = as.integer(btfp == 1 & dw == 1),
btfp_only = as.integer(btfp == 1 & dw == 0),
dw_only = as.integer(btfp == 0 & dw == 1),
facility_choice = factor(
case_when(both == 1 ~ "Both", btfp_only == 1 ~ "BTFP_Only",
dw_only == 1 ~ "DW_Only", TRUE ~ "Neither"),
levels = c("Neither", "BTFP_Only", "DW_Only", "Both")
),
# Period-specific entry
btfp_acute = as.integer(!is.na(btfp_first_period) & btfp_first_period == 2),
btfp_post = as.integer(!is.na(btfp_first_period) & btfp_first_period == 3),
btfp_arb = as.integer(!is.na(btfp_first_period) & btfp_first_period == 4),
# DW period-specific
dw_acute = as.integer(!is.na(dw_first_period) & dw_first_period == 2),
dw_post = as.integer(!is.na(dw_first_period) & dw_first_period == 3),
dw_arb = as.integer(!is.na(dw_first_period) & dw_first_period == 4),
# ================================================================
# RAW KEY VARIABLES (will be winsorized)
# ================================================================
# MTM Loss Variables - CRITICAL: Split by collateral eligibility
mtm_btfp_raw = mtm_loss_omo_eligible_to_total_asset,
mtm_other_raw = mtm_loss_non_omo_eligible_to_total_asset,
mtm_total_raw = mtm_loss_to_total_asset,
# Borrowing subsidy (par valuation benefit)
borrowing_subsidy_raw = mtm_loss_omo_eligible_to_omo_eligible,
# Deposit structure
uninsured_lev_raw = uninsured_deposit_to_total_asset,
uninsured_share_raw = uninsured_to_deposit,
# Collateral ratios
omo_ratio_raw = omo_eligible_to_total_asset,
non_omo_ratio_raw = non_omo_eligible_to_total_asset,
# ================================================================
# JIANG ET AL. INSOLVENCY MEASURES (RAW)
# ================================================================
mv_asset = mm_asset,
# Adjusted Equity = Book Equity - MTM Losses
adjusted_equity_raw = book_equity_to_total_asset - mtm_loss_to_total_asset,
# Market value adjustment factor
mv_adjustment = safe_div(total_asset - mv_asset, mv_asset, 0),
# Insured Deposit Coverage Ratio (IDCR)
# IDCR = (MV_Assets - s*Uninsured - Insured) / Insured
idcr_50_raw = safe_div(mv_asset - 0.5 * uninsured_deposit - insured_deposit, insured_deposit),
idcr_100_raw = safe_div(mv_asset - 1.0 * uninsured_deposit - insured_deposit, insured_deposit),
# Capital ratio under run scenario
insolvency_50_raw = safe_div(
(total_asset - total_liability) - 0.5 * uninsured_deposit * mv_adjustment,
total_asset
),
insolvency_100_raw = safe_div(
(total_asset - total_liability) - 1.0 * uninsured_deposit * mv_adjustment,
total_asset
),
# ================================================================
# CONTROL VARIABLES (RAW)
# ================================================================
ln_assets_raw = log(total_asset),
cash_ratio_raw = cash_to_total_asset,
securities_ratio_raw = security_to_total_asset,
loan_ratio_raw = total_loan_to_total_asset,
book_equity_ratio_raw = book_equity_to_total_asset,
tier1_ratio_raw = tier1cap_to_total_asset,
roa_raw = roa,
loan_to_deposit_raw = loan_to_deposit,
fhlb_ratio_raw = fhlb_to_total_asset,
# Wholesale funding
wholesale_raw = safe_div(
fed_fund_purchase + repo + replace_na(other_borrowed_less_than_1yr, 0),
total_liability, 0
) * 100,
# Runable funding
runable_raw = safe_div(
uninsured_deposit + fed_fund_purchase + repo + replace_na(other_borrowed_less_than_1yr, 0),
total_liability, 0
) * 100,
# ================================================================
# INTENSIVE MARGIN (RAW)
# ================================================================
btfp_pct_raw = ifelse(btfp == 1, 100 * btfp_amount / (total_asset * 1000), NA_real_),
dw_pct_raw = ifelse(dw == 1, 100 * dw_amount / (total_asset * 1000), NA_real_),
btfp_util_raw = ifelse(btfp == 1 & omo_eligible > 0,
btfp_amount / (omo_eligible * 1000), NA_real_),
# Size category
size_cat = factor(size_bin, levels = c("small", "large")),
# Prior DW user (revealed preference / operational readiness)
prior_dw = dw_pre
)
# ============================================================================
# WINSORIZE ALL CONTINUOUS VARIABLES AT 1-99%
# CRITICAL: Use winsorized versions in ALL analysis
# ============================================================================
df <- df_raw %>%
mutate(
# Key explanatory (WINSORIZED)
mtm_btfp = winsorize(mtm_btfp_raw),
mtm_other = winsorize(mtm_other_raw),
mtm_total = winsorize(mtm_total_raw),
borrowing_subsidy = winsorize(borrowing_subsidy_raw),
uninsured_lev = winsorize(uninsured_lev_raw),
uninsured_share = winsorize(uninsured_share_raw),
# Collateral (WINSORIZED)
omo_ratio = winsorize(omo_ratio_raw),
non_omo_ratio = winsorize(non_omo_ratio_raw),
# Insolvency measures (WINSORIZED)
adjusted_equity = winsorize(adjusted_equity_raw),
idcr_50 = winsorize(idcr_50_raw),
idcr_100 = winsorize(idcr_100_raw),
insolvency_50 = winsorize(insolvency_50_raw),
insolvency_100 = winsorize(insolvency_100_raw),
# Controls (WINSORIZED)
ln_assets = winsorize(ln_assets_raw),
cash_ratio = winsorize(cash_ratio_raw),
securities_ratio = winsorize(securities_ratio_raw),
loan_ratio = winsorize(loan_ratio_raw),
book_equity_ratio = winsorize(book_equity_ratio_raw),
tier1_ratio = winsorize(tier1_ratio_raw),
roa = winsorize(roa_raw),
loan_to_deposit = winsorize(loan_to_deposit_raw),
fhlb_ratio = winsorize(fhlb_ratio_raw),
wholesale = winsorize(wholesale_raw),
runable = winsorize(runable_raw),
# Intensive margin (WINSORIZED)
btfp_pct = winsorize(btfp_pct_raw),
dw_pct = winsorize(dw_pct_raw),
btfp_util = winsorize(btfp_util_raw),
# ================================================================
# DERIVED VARIABLES (from winsorized inputs)
# ================================================================
# Run risk interaction
run_risk = uninsured_lev * mtm_total,
# Insolvency dummies
mtm_insolvent = as.integer(adjusted_equity < 0),
insolvent_idcr_50 = as.integer(idcr_50 < 0),
insolvent_idcr_100 = as.integer(idcr_100 < 0),
# Risk dummies (based on winsorized medians)
high_uninsured = as.integer(uninsured_lev > median(uninsured_lev, na.rm = TRUE)),
high_mtm = as.integer(mtm_total > median(mtm_total, na.rm = TRUE)),
high_mtm_btfp = as.integer(mtm_btfp > median(mtm_btfp, na.rm = TRUE)),
run_risk_dummy = as.integer(high_uninsured == 1 & high_mtm == 1),
# Maxed out BTFP
maxed_out = as.integer(!is.na(btfp_util) & btfp_util > 0.90),
# DW capacity gap (what BTFP provided that DW couldn't)
# = BTFP amount - (what DW would have lent on same collateral with haircuts)
dw_capacity_gap = ifelse(btfp == 1 & !is.na(btfp_amount) & !is.na(borrowing_subsidy),
btfp_amount * borrowing_subsidy / 100, NA_real_)
)
# Remove raw variables to prevent accidental use
df <- df %>% select(-ends_with("_raw"))
cat("\n=== FINAL SAMPLE (WINSORIZED) ===\n")
=== FINAL SAMPLE (WINSORIZED) ===
cat("Total banks:", nrow(df), "\n")
Total banks: 4696
cat("BTFP users:", sum(df$btfp), "\n")
BTFP users: 1305
cat("DW users:", sum(df$dw), "\n")
DW users: 1067
cat("Both:", sum(df$both), "\n")
Both: 425
cat("DW pre-BTFP (placebo):", sum(df$dw_pre), "\n")
DW pre-BTFP (placebo): 1308
cat("MTM insolvent:", sum(df$mtm_insolvent, na.rm = TRUE), "\n")
MTM insolvent: 864
Main Regression
Analysis
Global Model
Settings
# ============================================================================
# GLOBAL MODEL SETTINGS
# ============================================================================
# Standard controls (ALL WINSORIZED)
CONTROLS <- "ln_assets + cash_ratio + securities_ratio + loan_to_deposit + book_equity_ratio + wholesale + fhlb_ratio + roa"
# Coefficient labels
COEF_MAP <- c(
"mtm_total" = "MTM Loss (Total)",
"mtm_btfp" = "MTM Loss (BTFP-Eligible)",
"mtm_other" = "MTM Loss (Non-BTFP)",
"uninsured_lev" = "Uninsured Leverage",
"borrowing_subsidy" = "Borrowing Subsidy",
"run_risk" = "Run Risk (MTM Ă— Uninsured)",
"I(mtm_btfp * uninsured_lev)" = "MTM(BTFP) Ă— Uninsured",
"I(mtm_total * uninsured_lev)" = "MTM Ă— Uninsured",
"adjusted_equity" = "Adjusted Equity",
"mtm_insolvent" = "MTM Insolvent (Dummy)",
"idcr_100" = "IDCR (100% run)",
"insolvent_idcr_100" = "Insolvent (IDCR<0)",
"prior_dw" = "Prior DW User",
"omo_ratio" = "OMO-Eligible Ratio",
"non_omo_ratio" = "Non-OMO Ratio",
"maxed_out" = "Maxed Out (>90%)",
"high_uninsured" = "High Uninsured (Dummy)",
"high_mtm_btfp" = "High MTM-BTFP (Dummy)",
"ln_assets" = "Log(Assets)",
"cash_ratio" = "Cash Ratio",
"securities_ratio" = "Securities Ratio",
"loan_to_deposit" = "Loan/Deposit",
"book_equity_ratio" = "Book Equity",
"wholesale" = "Wholesale Funding",
"fhlb_ratio" = "FHLB Ratio",
"roa" = "ROA"
)
cat("=== MODEL SETTINGS ===\n")
=== MODEL SETTINGS ===
cat("Controls:", CONTROLS, "\n")
Controls: ln_assets + cash_ratio + securities_ratio + loan_to_deposit
+ book_equity_ratio + wholesale + fhlb_ratio + roa
cat("Sample size:", nrow(df), "\n")
Sample size: 4696
Table 4: Main
Extensive Margin - Par Valuation Test
Causal Test: If par valuation drives BTFP selection,
MTM(BTFP-eligible) should predict BTFP; MTM(Non-eligible) should
NOT.
# ============================================================================
# TABLE 4: MAIN EXTENSIVE MARGIN - PAR VALUATION TEST
# ============================================================================
f1 <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f2 <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f3 <- as.formula(paste("dw ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f4 <- as.formula(paste("any_fed ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f5 <- as.formula(paste("both ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
m4_1 <- feols(f1, data = df, vcov = "hetero")
m4_2 <- feols(f2, data = df, vcov = "hetero")
m4_3 <- feols(f3, data = df, vcov = "hetero")
m4_4 <- feols(f4, data = df, vcov = "hetero")
m4_5 <- feols(f5, data = df, vcov = "hetero")
modelsummary(
list("(1) BTFP" = m4_1, "(2) BTFP Int" = m4_2, "(3) DW" = m4_3,
"(4) Any Fed" = m4_4, "(5) Both" = m4_5),
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = COEF_MAP,
gof_map = c("nobs", "r.squared"),
title = "Table 4: Main Extensive Margin - Par Valuation Test",
notes = list("Robust SE. All variables winsorized.",
"KEY TEST: MTM(BTFP-Eligible) should predict BTFP but NOT DW.")
)
Table 4: Main Extensive Margin - Par Valuation Test
| |
(1) BTFP |
(2) BTFP Int |
(3) DW |
(4) Any Fed |
(5) Both |
| * p < 0.1, ** p < 0.05, *** p < 0.01 |
| Robust SE. All variables winsorized. |
| KEY TEST: MTM(BTFP-Eligible) should predict BTFP but NOT DW. |
| MTM Loss (BTFP-Eligible) |
0.044*** |
0.065*** |
0.009 |
0.055*** |
0.009 |
|
(0.011) |
(0.019) |
(0.009) |
(0.020) |
(0.013) |
| MTM Loss (Non-BTFP) |
0.010*** |
0.010*** |
0.001 |
0.009** |
0.003 |
|
(0.003) |
(0.003) |
(0.003) |
(0.004) |
(0.002) |
| Uninsured Leverage |
0.002*** |
0.003*** |
0.001 |
0.002** |
0.001** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
(0.000) |
| MTM(BTFP) Ă— Uninsured |
|
-0.001 |
|
-0.001 |
0.000 |
|
|
(0.001) |
|
(0.001) |
(0.001) |
| Log(Assets) |
0.054*** |
0.054*** |
0.094*** |
0.104*** |
0.043*** |
|
(0.006) |
(0.006) |
(0.005) |
(0.006) |
(0.004) |
| Cash Ratio |
-0.005*** |
-0.005*** |
-0.000 |
-0.004*** |
-0.001*** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
(0.000) |
| Securities Ratio |
0.002** |
0.002* |
0.000 |
0.002* |
0.000 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
(0.000) |
| Loan/Deposit |
-0.001** |
-0.001** |
0.001 |
0.000 |
-0.001* |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
(0.000) |
| Book Equity |
-0.002*** |
-0.002*** |
0.000 |
-0.002*** |
0.001 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
(0.000) |
| Wholesale Funding |
0.009*** |
0.009*** |
0.002 |
0.005 |
0.006** |
|
(0.003) |
(0.003) |
(0.003) |
(0.003) |
(0.002) |
| FHLB Ratio |
0.007*** |
0.007*** |
0.001 |
0.005** |
0.004*** |
|
(0.002) |
(0.002) |
(0.002) |
(0.002) |
(0.001) |
| ROA |
-0.000 |
-0.000 |
0.002 |
0.006 |
-0.004 |
|
(0.007) |
(0.007) |
(0.007) |
(0.008) |
(0.004) |
| Num.Obs. |
4678 |
4678 |
4678 |
4678 |
4678 |
| R2 |
0.107 |
0.107 |
0.120 |
0.156 |
0.077 |
Table 5: Borrowing
Subsidy Specification
# ============================================================================
# TABLE 5: BORROWING SUBSIDY SPECIFICATION
# ============================================================================
f_sub1 <- as.formula(paste("btfp ~ borrowing_subsidy + uninsured_lev +", CONTROLS))
f_sub2 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))
f_sub3 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev + I(borrowing_subsidy * uninsured_lev) +", CONTROLS))
f_sub4 <- as.formula(paste("dw ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))
m5_1 <- feols(f_sub1, data = df, vcov = "hetero")
m5_2 <- feols(f_sub2, data = df, vcov = "hetero")
m5_3 <- feols(f_sub3, data = df, vcov = "hetero")
m5_4 <- feols(f_sub4, data = df, vcov = "hetero")
modelsummary(
list("(1) Subsidy" = m5_1, "(2) + Non-BTFP" = m5_2,
"(3) Interaction" = m5_3, "(4) DW" = m5_4),
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = COEF_MAP,
gof_map = c("nobs", "r.squared"),
title = "Table 5: Borrowing Subsidy and Facility Selection",
notes = "Borrowing Subsidy = MTM Loss Rate on OMO-Eligible (par valuation benefit)"
)
Table 5: Borrowing Subsidy and Facility Selection
| |
(1) Subsidy |
(2) + Non-BTFP |
(3) Interaction |
(4) DW |
| * p < 0.1, ** p < 0.05, *** p < 0.01 |
| Borrowing Subsidy = MTM Loss Rate on OMO-Eligible (par valuation benefit) |
| MTM Loss (Non-BTFP) |
|
0.006* |
0.006* |
-0.001 |
|
|
(0.004) |
(0.004) |
(0.003) |
| Uninsured Leverage |
0.002** |
0.002** |
0.002** |
0.001 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Borrowing Subsidy |
0.003* |
0.002 |
0.003 |
0.002* |
|
(0.001) |
(0.001) |
(0.003) |
(0.001) |
| Log(Assets) |
0.058*** |
0.057*** |
0.057*** |
0.092*** |
|
(0.006) |
(0.006) |
(0.006) |
(0.006) |
| Cash Ratio |
-0.005*** |
-0.005*** |
-0.005*** |
-0.001 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Securities Ratio |
0.004*** |
0.004*** |
0.004*** |
-0.000 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Loan/Deposit |
-0.000 |
-0.000 |
-0.000 |
0.001 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Book Equity |
-0.003*** |
-0.003*** |
-0.003*** |
0.000 |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Wholesale Funding |
0.007* |
0.007** |
0.007** |
0.002 |
|
(0.003) |
(0.003) |
(0.003) |
(0.003) |
| FHLB Ratio |
0.008*** |
0.008*** |
0.008*** |
0.000 |
|
(0.002) |
(0.002) |
(0.002) |
(0.002) |
| ROA |
-0.003 |
-0.002 |
-0.002 |
0.001 |
|
(0.008) |
(0.008) |
(0.008) |
(0.008) |
| Num.Obs. |
4282 |
4282 |
4282 |
4282 |
| R2 |
0.097 |
0.097 |
0.097 |
0.120 |
Table 6: Placebo Test
- Pre-BTFP DW Usage
Causal Test: If MTM(BTFP-eligible) matters because
of par valuation, it should NOT predict DW usage BEFORE BTFP
existed.
# ============================================================================
# TABLE 6: PLACEBO TEST - PRE-BTFP DW USAGE
# ============================================================================
f_pla1 <- as.formula(paste("dw_pre ~ mtm_total + uninsured_lev +", CONTROLS))
f_pla2 <- as.formula(paste("dw_pre ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f_pla3 <- as.formula(paste("dw_pre ~ mtm_total + uninsured_lev + I(mtm_total * uninsured_lev) +", CONTROLS))
m6_1 <- feols(f_pla1, data = df, vcov = "hetero")
m6_2 <- feols(f_pla2, data = df, vcov = "hetero")
m6_3 <- feols(f_pla3, data = df, vcov = "hetero")
modelsummary(
list("(1) Placebo: Total" = m6_1, "(2) Placebo: Split" = m6_2,
"(3) Placebo: Int" = m6_3, "(4) Main: BTFP" = m4_2),
stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
coef_map = COEF_MAP,
gof_map = c("nobs", "r.squared"),
title = "Table 6: Placebo Test - Pre-BTFP DW vs Post-BTFP BTFP",
notes = list("Placebo: DW usage Mar 1-12, 2023 (before BTFP existed).",
"IDENTIFICATION: MTM(BTFP) should matter MORE for BTFP than for pre-BTFP DW.")
)
Table 6: Placebo Test - Pre-BTFP DW vs Post-BTFP BTFP
| |
(1) Placebo: Total |
(2) Placebo: Split |
(3) Placebo: Int |
(4) Main: BTFP |
| * p < 0.1, ** p < 0.05, *** p < 0.01 |
| Placebo: DW usage Mar 1-12, 2023 (before BTFP existed). |
| IDENTIFICATION: MTM(BTFP) should matter MORE for BTFP than for pre-BTFP DW. |
| MTM Loss (Total) |
0.002 |
|
-0.006 |
|
|
(0.003) |
|
(0.005) |
|
| MTM Loss (BTFP-Eligible) |
|
0.010 |
|
0.065*** |
|
|
(0.009) |
|
(0.019) |
| MTM Loss (Non-BTFP) |
|
0.004 |
|
0.010*** |
|
|
(0.003) |
|
(0.003) |
| Uninsured Leverage |
-0.001 |
-0.001 |
-0.003** |
0.003*** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| MTM(BTFP) Ă— Uninsured |
|
|
|
-0.001 |
|
|
|
|
(0.001) |
| MTM Ă— Uninsured |
|
|
0.000* |
|
|
|
|
(0.000) |
|
| Log(Assets) |
0.141*** |
0.139*** |
0.140*** |
0.054*** |
|
(0.005) |
(0.005) |
(0.005) |
(0.006) |
| Cash Ratio |
0.000 |
0.000 |
0.000 |
-0.005*** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Securities Ratio |
0.000 |
0.000 |
0.001 |
0.002* |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Loan/Deposit |
0.000 |
0.000 |
0.000 |
-0.001** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Book Equity |
0.000 |
0.001 |
0.000 |
-0.002*** |
|
(0.001) |
(0.001) |
(0.001) |
(0.001) |
| Wholesale Funding |
0.004 |
0.004 |
0.004 |
0.009*** |
|
(0.003) |
(0.003) |
(0.003) |
(0.003) |
| FHLB Ratio |
0.003** |
0.003* |
0.003* |
0.007*** |
|
(0.002) |
(0.002) |
(0.002) |
(0.002) |
| ROA |
-0.008 |
-0.007 |
-0.009 |
-0.000 |
|
(0.007) |
(0.007) |
(0.007) |
(0.007) |
| Num.Obs. |
4678 |
4678 |
4678 |
4678 |
| R2 |
0.200 |
0.200 |
0.200 |
0.107 |
Conclusion
This analysis provides evidence for the causal mechanisms driving
bank facility choice during the 2023 banking crisis:
Par Valuation Creates Selection: Banks with
larger MTM losses on BTFP-eligible securities systematically selected
BTFP to capture the par valuation subsidy, while MTM losses on
non-eligible securities did not predict BTFP usage.
Placebo Validates Identification: The effect is
weaker for pre-BTFP DW usage when par valuation didn’t exist, supporting
our identification strategy.
Vulnerable Banks Accessed Facilities:
MTM-insolvent banks (negative adjusted equity) were more likely to
access Fed facilities, with BTFP providing particular value.
Borrowing Determinants Evolved: From run-driven
(acute) to subsidy-driven (post-acute) to rate-driven
(arbitrage).
BTFP Filled a Gap: BTFP provided approximately
15% more liquidity than DW could have on the same collateral, addressing
DW insufficiency.
Analysis completed: December 26, 2025 at 08:00
R version 4.3.1 (2023-06-16 ucrt) Platform: x86_64-w64-mingw32/x64
(64-bit) Running under: Windows 11 x64 (build 26200)
Matrix products: default
locale: [1] LC_COLLATE=English_United States.utf8
LC_CTYPE=English_United States.utf8 LC_MONETARY=English_United
States.utf8 [4] LC_NUMERIC=C 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] psych_2.5.6 viridis_0.6.5
viridisLite_0.4.2 scales_1.4.0 patchwork_1.3.2
[6] kableExtra_1.4.0 modelsummary_2.4.0 sampleSelection_1.2-12
maxLik_1.5-2.1 miscTools_0.6-28
[11] pROC_1.18.5 margins_0.3.28 broom_1.0.9 lmtest_0.9-40
zoo_1.8-13
[16] sandwich_3.1-1 fixest_0.12.1 data.table_1.17.0 lubridate_1.9.4
forcats_1.0.0
[21] stringr_1.5.1 dplyr_1.1.4 purrr_1.0.4 readr_2.1.5 tidyr_1.3.1
[26] tibble_3.2.1 ggplot2_3.5.2 tidyverse_2.0.0
loaded via a namespace (and not attached): [1] mnormt_2.1.1
gridExtra_2.3 rlang_1.1.1 magrittr_2.0.3 dreamerr_1.4.0
[6] prediction_0.3.18 compiler_4.3.1 systemfonts_1.2.2 vctrs_0.6.5
pkgconfig_2.0.3
[11] crayon_1.5.3 fastmap_1.2.0 backports_1.5.0 labeling_0.4.3
rmarkdown_2.29
[16] tzdb_0.5.0 bit_4.6.0 xfun_0.52 cachem_1.1.0 litedown_0.7
[21] jsonlite_2.0.0 tinytable_0.13.0 stringmagic_1.1.2 systemfit_1.1-30
VGAM_1.1-13
[26] parallel_4.3.1 R6_2.6.1 bslib_0.9.0 tables_0.9.31
stringi_1.8.7
[31] RColorBrewer_1.1-3 car_3.1-3 jquerylib_0.1.4 numDeriv_2016.8-1.1
estimability_1.5.1 [36] Rcpp_1.0.14 knitr_1.50 parameters_0.27.0
Matrix_1.5-4.1 splines_4.3.1
[41] timechange_0.3.0 tidyselect_1.2.1 rstudioapi_0.17.1 abind_1.4-8
yaml_2.3.10
[46] lattice_0.21-8 plyr_1.8.9 withr_3.0.2 bayestestR_0.16.1
coda_0.19-4.1
[51] evaluate_1.0.4 xml2_1.3.8 pillar_1.11.0 carData_3.0-5
checkmate_2.3.2
[56] stats4_4.3.1 insight_1.3.1 generics_0.1.4 vroom_1.6.5
hms_1.1.3
[61] xtable_1.8-4 glue_1.8.0 emmeans_1.11.2-8 tools_4.3.1
mvtnorm_1.3-3
[66] grid_4.3.1 datawizard_1.2.0 nlme_3.1-162 performance_0.15.0
Formula_1.2-5
[71] cli_3.6.1 fansi_1.0.6 svglite_2.1.3 gtable_0.3.6 sass_0.4.10
[76] digest_0.6.33 farver_2.1.2 htmltools_0.5.9 lifecycle_1.0.4
bit64_4.6.0-1
[81] MASS_7.3-60