Theoretical Framework
& Model Map
Four-model identification strategy for emergency lending
facility selection.
Model 1 — Main Results (Crisis Period, DSSW Solvency
Split)
Solvency is defined using the Drechsler, Savov, Schnabl & Wang
(2023, 2025) deposit franchise value (DFV) framework. The DFV captures
the present value of below-market deposit funding. In cross-section, the
bank-specific DFV per dollar of assets is:
\[\text{DFV}_i = (1 - \beta_i) \times
\frac{D_i}{TA_i}\]
where \(\beta_i\) is the bank’s
deposit beta (sensitivity of deposit rate to the Fed Funds rate). The
full DSSW formula is \(D \times [(1 - \beta) -
c/r] \times [1 - 1/(1+r)^T]\) where \(c\) is operating cost (\(\approx 2\%\)), \(r\) is the long rate, and \(T\) is the deposit runoff horizon. Since
\(c\), \(r\), and \(T\) are approximately constant in
cross-section, all variation comes from \(\beta_i\) and \(D_i/TA_i\), justifying our
simplification.
DSSW Adjusted Equity = Book Equity/TA \(-\) MTM Loss/TA \(+\) DFV/TA
A bank is DSSW-solvent if DSSW Adjusted Equity \(\geq 0\), DSSW-insolvent
otherwise.
For each borrower type \(\in
\{\text{AnyFed, BTFP, DW, FHLB}\}\) and each solvency class \(\in \{\text{Solvent, Insolvent}\}\) (8
regressions):
\[\text{Borrower}_i = \alpha + \beta_1
\text{MTM}_i + \beta_2 \text{Uninsured}_i + \beta_3 (\text{MTM}_i \times
\text{Uninsured}_i) + \gamma \mathbf{X}_i + \varepsilon_i\]
Model 2 — BTFP & DW: Crisis vs. Arbitrage (DSSW Solvency
Split)
Same specification, estimated separately for BTFP and DW across
Crisis and Arbitrage periods, split by DSSW solvency. This tests whether
the panic mechanism (MTM \(\times\)
Uninsured) operates only during the crisis window.
Model 3 — Deposit Beta Channel
Introduce uninsured_beta (\(\beta^U_i\) from DSSW estimation) and a
triple interaction:
\[\text{Borrower}_i = \alpha + \beta_1
\text{MTM}_i + \beta_2 \text{Uninsured}_i + \beta_3 \text{Unins\_Beta}_i
+ \beta_4 (\text{MTM}_i \times \text{Uninsured}_i) + \beta_5
(\text{MTM}_i \times \text{Uninsured}_i \times \text{Unins\_Beta}_i) +
\gamma \mathbf{X}_i + \varepsilon_i\]
Banks with low uninsured beta have stickier (less rate-sensitive)
uninsured deposits. The triple interaction tests whether the panic
mechanism is amplified when uninsured deposits are also “flighty” (high
\(\beta^U\)).
Model 4 — BTFP Complementarity with DW
All BTFP borrowers are DW-eligible by definition. The DW accepts
broader collateral but lends at market value; the BTFP lends at par but
only against OMO-eligible securities. BTFP complements DW by: (a)
removing fire-sale pressure on liquid securities, (b) longer term (up to
1 year vs. overnight), (c) reduced stigma.
Modeling approach: - 4a. Facility Choice
(Multinomial): Among all emergency borrowers during Crisis,
model the choice \(\{\)BTFP Only, DW
Only, Both\(\}\) as a function of bank
characteristics. - 4b. Complementarity Test: Among DW
borrowers, model additional BTFP usage (\(\text{Both} = 1\) vs. \(\text{DW\_Only} = 0\)). Prediction: banks
with larger OMO MTM losses (higher par benefit) choose BTFP as
complement. - 4c. Substitution Test: Among all
emergency borrowers, model BTFP-only (\(=
1\)) vs. DW-only (\(= 0\)).
Prediction: banks with more OMO-eligible collateral and larger par
subsidy choose BTFP over DW.
SETUP
Packages
rm(list = ls())
# Core
library(data.table)
library(dplyr)
library(tidyr)
library(stringr)
library(lubridate)
library(purrr)
library(tibble)
library(ggrepel)
# Econometrics
library(fixest)
library(marginaleffects)
library(nnet)
library(broom)
# Tables
library(modelsummary)
library(knitr)
library(kableExtra)
# Statistics
library(DescTools)
# Visualization
library(ggplot2)
library(gridExtra)
library(scales)
library(patchwork)
# I/O
library(readr)
library(readxl)
cat("All packages loaded.\n")
## All packages loaded.
Helper Functions
# ==============================================================================
# CORE HELPERS
# ==============================================================================
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)
}
create_size_category_3 <- function(assets_thousands) {
assets_millions <- assets_thousands / 1000
case_when(
assets_millions >= 100000 ~ "Large (>$100B)",
assets_millions >= 1000 ~ "Medium ($1B-$100B)",
TRUE ~ "Small (<$1B)"
)
}
size_levels_3 <- c("Small (<$1B)", "Medium ($1B-$100B)", "Large (>$100B)")
format_pval <- function(p) {
case_when(is.na(p) ~ "", p < 0.01 ~ "***", p < 0.05 ~ "**", p < 0.10 ~ "*", TRUE ~ "")
}
# ==============================================================================
# FILE I/O
# ==============================================================================
safe_writeLines <- function(text, con, max_retries = 5, wait_sec = 2) {
for (i in seq_len(max_retries)) {
result <- tryCatch(
{ writeLines(text, con); TRUE },
error = function(e) {
if (i < max_retries) Sys.sleep(wait_sec)
FALSE
}
)
if (isTRUE(result)) return(invisible(NULL))
}
warning("Failed to write ", con, " after ", max_retries, " attempts.")
}
save_table <- function(tbl, filename, caption_text = "") {
latex_file <- file.path(TABLE_PATH, paste0(filename, ".tex"))
latex_content <- knitr::kable(
tbl, format = "latex", caption = caption_text, booktabs = TRUE
)
safe_writeLines(as.character(latex_content), latex_file)
message("Saved: ", filename, ".tex")
}
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"
)
message("Saved: ", filename, ".pdf")
}
Paths
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/Four_Model_Analysis")
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
BASELINE_MAIN <- "2022Q4"
BASELINE_ARB <- "2023Q3"
BASELINE_WIND <- "2023Q4"
# Analysis windows
CRISIS_START <- as.Date("2023-03-08")
CRISIS_END <- as.Date("2023-05-04")
ARB_START <- as.Date("2023-11-15")
ARB_END <- as.Date("2024-01-24")
WIND_START <- as.Date("2024-01-25")
WIND_END <- as.Date("2024-03-11")
DW_DATA_END <- as.Date("2023-12-31")
cat("=== DATE CONSTANTS SET ===\n")
## === DATE CONSTANTS SET ===
cat("Crisis window: ", format(CRISIS_START), "to", format(CRISIS_END), "\n")
## Crisis window: 2023-03-08 to 2023-05-04
cat("Arbitrage window: ", format(ARB_START), "to", format(ARB_END), "\n")
## Arbitrage window: 2023-11-15 to 2024-01-24
DATA LOADING
# --- Call Reports ---
call_q <- read_csv(
file.path(DATA_PROC, "final_call_gsib.csv"),
show_col_types = FALSE
) %>%
mutate(idrssd = as.character(idrssd))
# --- BTFP Loan-Level ---
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)
)
# --- Discount Window Loan-Level ---
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)
)
# --- DSSW Deposit Betas ---
dssw_betas <- read_csv(
file.path(DATA_PROC, "dssw_deposit_betas.csv"),
show_col_types = FALSE
) %>%
mutate(idrssd = as.character(idrssd))
# Pre-split betas by estimation period
dssw_beta_2022q4 <- dssw_betas %>% filter(estimation_date == "2022Q4") %>%
select(idrssd, beta_overall, beta_insured_w, beta_uninsured_w,
gamma_hat, alpha_hat)
dssw_beta_2023q4 <- dssw_betas %>% filter(estimation_date == "2023Q4") %>%
select(idrssd, beta_overall, beta_insured_w, beta_uninsured_w,
gamma_hat, alpha_hat)
cat("=== DATA LOADED ===\n")
## === DATA LOADED ===
cat("Call Report: ", nrow(call_q), "obs |",
n_distinct(call_q$idrssd), "banks\n")
## Call Report: 75989 obs | 5074 banks
cat("DSSW Betas: ", nrow(dssw_betas), "obs |",
n_distinct(dssw_betas$idrssd), "banks\n")
## DSSW Betas: 13588 obs | 4660 banks
cat("BTFP Loans: ", nrow(btfp_loans_raw), "loans\n")
## BTFP Loans: 6734 loans
cat("DW Loans: ", nrow(dw_loans_raw), "loans\n")
## DW Loans: 10008 loans
Exclusions
excluded_banks <- call_q %>%
filter(period == BASELINE_MAIN, failed_bank == 1 | gsib == 1) %>%
pull(idrssd)
cat("Excluded banks (failed + G-SIBs):", length(excluded_banks), "\n")
## Excluded banks (failed + G-SIBs): 41
btfp_loans <- btfp_loans_raw %>% filter(!rssd_id %in% excluded_banks)
dw_loans <- dw_loans_raw %>% filter(!rssd_id %in% excluded_banks)
BORROWER INDICATOR
FACTORY
create_borrower_indicator <- function(loans_df, date_col, id_col, amount_col,
start_date, end_date, prefix) {
loans_df %>%
filter(
!!sym(date_col) >= start_date,
!!sym(date_col) <= end_date
) %>%
group_by(!!sym(id_col)) %>%
summarise(
"{prefix}" := 1L,
"{prefix}_amt" := sum(!!sym(amount_col), na.rm = TRUE),
"{prefix}_first" := min(!!sym(date_col)),
.groups = "drop"
) %>%
rename(idrssd = !!sym(id_col))
}
# --- BTFP ---
btfp_crisis <- create_borrower_indicator(
btfp_loans, "btfp_loan_date", "rssd_id", "btfp_loan_amount",
CRISIS_START, CRISIS_END, "btfp_crisis"
)
btfp_arb <- create_borrower_indicator(
btfp_loans, "btfp_loan_date", "rssd_id", "btfp_loan_amount",
ARB_START, ARB_END, "btfp_arb"
)
# --- DW ---
dw_crisis <- create_borrower_indicator(
dw_loans, "dw_loan_date", "rssd_id", "dw_loan_amount",
CRISIS_START, min(CRISIS_END, DW_DATA_END), "dw_crisis"
)
dw_arb <- create_borrower_indicator(
dw_loans, "dw_loan_date", "rssd_id", "dw_loan_amount",
ARB_START, DW_DATA_END, "dw_arb"
)
cat("=== BORROWER COUNTS ===\n")
## === BORROWER COUNTS ===
cat("BTFP: Crisis=", nrow(btfp_crisis), " Arb=", nrow(btfp_arb), "\n")
## BTFP: Crisis= 526 Arb= 780
cat("DW: Crisis=", nrow(dw_crisis), " Arb=", nrow(dw_arb), "\n")
## DW: Crisis= 459 Arb= 389
VARIABLE
CONSTRUCTION
# ==============================================================================
# Variable naming convention:
# *_raw — original scale (for descriptive tables)
# *_w — winsorized at [2.5%, 97.5%]
# * — z-standardized (mean 0, SD 1) — used in all regressions
# ==============================================================================
construct_analysis_vars <- function(baseline_data) {
baseline_data %>%
mutate(
# ======================================================================
# 1. RAW VARIABLES
# ======================================================================
mtm_total_raw = mtm_loss_to_total_asset,
mtm_btfp_raw = mtm_loss_omo_eligible_to_total_asset,
mtm_other_raw = mtm_loss_non_omo_eligible_to_total_asset,
uninsured_lev_raw = uninsured_deposit_to_total_asset,
insured_lev_raw = r_insured_deposit,
uninsured_share_raw = uninsured_to_deposit,
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,
fhlb_ratio_raw = fhlb_to_total_asset,
loan_to_deposit_raw = loan_to_deposit,
wholesale_raw = safe_div(
fed_fund_purchase + repo +
replace_na(other_borrowed_less_than_1yr, 0),
total_liability, 0
) * 100,
uninsured_outflow_raw = change_uninsured_fwd_q,
insured_outflow_raw = change_insured_deposit_fwd_q,
total_outflow_raw = change_total_deposit_fwd_q,
# ======================================================================
# 2. SOLVENCY MEASURES
# ======================================================================
# Jiang-style Adjusted Equity
adjusted_equity_raw = book_equity_to_total_asset - mtm_loss_to_total_asset,
# IDCR measures
mv_assets = total_asset * (1 - mtm_loss_to_total_asset / 100),
idcr_1 = safe_div(
mv_assets - 0.5 * uninsured_deposit - insured_deposit,
insured_deposit, NA_real_
),
idcr_2 = safe_div(
mv_assets - uninsured_deposit - insured_deposit,
insured_deposit, NA_real_
),
# Binary solvency indicators (Jiang-style)
mtm_insolvent = as.integer(adjusted_equity_raw < 0),
mtm_solvent = as.integer(adjusted_equity_raw >= 0),
insolvent_idcr_s50 = as.integer(idcr_1 < 0),
insolvent_idcr_s100 = as.integer(idcr_2 < 0),
# ======================================================================
# 2b. DSSW DEPOSIT FRANCHISE VALUE
#
# Full DSSW (2023/2025) formula:
# F = D × [(1-β) - c/r] × [1 - 1/(1+r)^T]
#
# Cross-sectional simplification (c, r, T constant across banks):
# DFV_i ∝ (1 - β_i) × D_i / TA_i
#
# This captures all bank-specific variation for regressions.
# DSSW Adjusted Equity = Book Equity/TA - MTM Loss/TA + DFV/TA
# ======================================================================
dfv_raw = ifelse(
!is.na(beta_overall),
(1 - beta_overall) * (dom_deposit / total_asset) * 100,
NA_real_
),
adjusted_equity_dssw_raw = ifelse(
!is.na(dfv_raw),
book_equity_to_total_asset - mtm_loss_to_total_asset + dfv_raw,
NA_real_
),
# DSSW solvency indicators
dssw_insolvent = as.integer(!is.na(adjusted_equity_dssw_raw) &
adjusted_equity_dssw_raw < 0),
dssw_solvent = as.integer(!is.na(adjusted_equity_dssw_raw) &
adjusted_equity_dssw_raw >= 0),
# ======================================================================
# 2c. UNINSURED DEPOSIT BETA (for Model 3)
# ======================================================================
uninsured_beta_raw = ifelse(
!is.na(beta_uninsured_w), beta_uninsured_w, NA_real_
),
# ======================================================================
# 3. WINSORIZED VARIABLES
# ======================================================================
mtm_total_w = winsorize(mtm_total_raw),
mtm_btfp_w = winsorize(mtm_btfp_raw),
mtm_other_w = winsorize(mtm_other_raw),
uninsured_lev_w = winsorize(uninsured_lev_raw),
insured_lev_w = winsorize(r_insured_deposit),
adjusted_equity_w = winsorize(adjusted_equity_raw),
dfv_w = winsorize(dfv_raw),
adjusted_equity_dssw_w = winsorize(adjusted_equity_dssw_raw),
uninsured_beta_w = winsorize(uninsured_beta_raw),
ln_assets_w = winsorize(ln_assets_raw),
cash_ratio_w = winsorize(cash_ratio_raw),
book_equity_ratio_w = winsorize(book_equity_ratio_raw),
roa_w = winsorize(roa_raw),
loan_to_deposit_w = winsorize(loan_to_deposit_raw),
wholesale_w = winsorize(wholesale_raw),
uninsured_outflow_w = winsorize(uninsured_outflow_raw),
insured_outflow_w = winsorize(insured_outflow_raw),
total_outflow_w = winsorize(total_outflow_raw),
# ======================================================================
# 4. Z-STANDARDIZED VARIABLES
# ======================================================================
mtm_total = standardize_z(mtm_total_w),
mtm_btfp = standardize_z(mtm_btfp_w),
mtm_other = standardize_z(mtm_other_w),
uninsured_lev = standardize_z(uninsured_lev_w),
insured_lev = standardize_z(insured_lev_w),
adjusted_equity = standardize_z(adjusted_equity_w),
dfv = standardize_z(dfv_w),
adjusted_equity_dssw = standardize_z(adjusted_equity_dssw_w),
uninsured_beta = standardize_z(uninsured_beta_w),
ln_assets = standardize_z(ln_assets_w),
cash_ratio = standardize_z(cash_ratio_w),
book_equity_ratio = standardize_z(book_equity_ratio_w),
roa = standardize_z(roa_w),
loan_to_deposit = standardize_z(loan_to_deposit_w),
wholesale = standardize_z(wholesale_w),
uninsured_outflow = standardize_z(uninsured_outflow_w),
insured_outflow = standardize_z(insured_outflow_w),
total_outflow = standardize_z(total_outflow_w),
# ======================================================================
# 5. INTERACTION TERMS
# ======================================================================
# Base: MTM × Uninsured
mtm_x_uninsured = mtm_total * uninsured_lev,
mtm_x_insured = mtm_total * insured_lev,
# DSSW: DSSW AE × Uninsured
dssw_ae_x_uninsured = adjusted_equity_dssw * uninsured_lev,
# Model 3: Deposit beta interactions
mtm_x_unins_beta = mtm_total * uninsured_beta,
unins_x_unins_beta = uninsured_lev * uninsured_beta,
mtm_x_unins_x_beta = mtm_total * uninsured_lev * uninsured_beta,
# ======================================================================
# 6. PAR BENEFIT AND COLLATERAL CAPACITY
# ======================================================================
par_benefit_raw = safe_div(
mtm_btfp_raw,
mtm_btfp_raw + 100 * safe_div(omo_eligible, total_asset * 1000, 0),
NA_real_
),
collateral_capacity_raw = safe_div(
omo_eligible, total_asset * 1000, 0
) * 100,
# ======================================================================
# 7. CATEGORICAL VARIABLES
# ======================================================================
size_cat = factor(
create_size_category_3(total_asset),
levels = size_levels_3
),
state = if ("state" %in% names(.)) state else NA_character_,
fed_district = if ("fed_district" %in% names(.)) fed_district else NA_character_
) %>%
mutate(
par_benefit_w = winsorize(par_benefit_raw),
par_benefit = standardize_z(par_benefit_w),
par_x_uninsured = par_benefit * uninsured_lev,
collateral_capacity_w = winsorize(collateral_capacity_raw),
collateral_capacity = standardize_z(collateral_capacity_w)
)
}
BUILD BASELINE
DATASETS
# --- 2022Q4 Baseline (Crisis analysis) ---
df_2022q4 <- call_q %>%
filter(
period == BASELINE_MAIN,
!idrssd %in% excluded_banks,
!is.na(omo_eligible) & omo_eligible > 0
) %>%
left_join(dssw_beta_2022q4, by = "idrssd") %>%
construct_analysis_vars()
# --- 2023Q3 Baseline (Arbitrage period) ---
df_2023q3 <- call_q %>%
filter(
period == BASELINE_ARB,
!idrssd %in% excluded_banks,
!is.na(omo_eligible) & omo_eligible > 0
) %>%
left_join(dssw_beta_2022q4, by = "idrssd") %>%
construct_analysis_vars()
cat("=== BASELINE DATASETS ===\n")
## === BASELINE DATASETS ===
cat("2022Q4:", nrow(df_2022q4), "banks |",
"DSSW Insolvent:", sum(df_2022q4$dssw_insolvent, na.rm = TRUE),
"| Beta coverage:", sum(!is.na(df_2022q4$beta_overall)),
"| Uninsured Beta coverage:", sum(!is.na(df_2022q4$uninsured_beta_raw)), "\n")
## 2022Q4: 4292 banks | DSSW Insolvent: 0 | Beta coverage: 4226 | Uninsured Beta coverage: 4226
cat("2023Q3:", nrow(df_2023q3), "banks\n")
## 2023Q3: 4214 banks
BUILD PERIOD
DATASETS
# ==============================================================================
# JOIN_ALL_BORROWERS HELPER
# ==============================================================================
join_all_borrowers <- function(df_base, btfp_df, dw_df, btfp_var, dw_var) {
df_base %>%
left_join(btfp_df %>% select(idrssd, starts_with(btfp_var)), by = "idrssd") %>%
left_join(dw_df %>% select(idrssd, starts_with(dw_var)), by = "idrssd") %>%
mutate(
"{btfp_var}" := replace_na(!!sym(btfp_var), 0L),
"{dw_var}" := replace_na(!!sym(dw_var), 0L),
fhlb_user = as.integer(abnormal_fhlb_borrowing_10pct == 1),
any_fed = as.integer(!!sym(btfp_var) == 1 | !!sym(dw_var) == 1),
both_fed = as.integer(!!sym(btfp_var) == 1 & !!sym(dw_var) == 1),
user_group = factor(
case_when(
both_fed == 1 ~ "Both",
!!sym(btfp_var) == 1 ~ "BTFP_Only",
!!sym(dw_var) == 1 ~ "DW_Only",
TRUE ~ "Neither"
),
levels = c("Neither", "BTFP_Only", "DW_Only", "Both")
),
non_user = as.integer(any_fed == 0 & fhlb_user == 0)
)
}
# ==============================================================================
# CRISIS PERIOD
# ==============================================================================
df_crisis <- join_all_borrowers(
df_2022q4, btfp_crisis, dw_crisis, "btfp_crisis", "dw_crisis"
) %>%
mutate(
btfp_pct = ifelse(
btfp_crisis == 1 & btfp_crisis_amt > 0,
100 * btfp_crisis_amt / (total_asset * 1000), NA_real_
),
dw_pct = ifelse(
dw_crisis == 1 & dw_crisis_amt > 0,
100 * dw_crisis_amt / (total_asset * 1000), NA_real_
),
log_btfp_amt = ifelse(
btfp_crisis == 1 & btfp_crisis_amt > 0, log(btfp_crisis_amt), NA_real_
),
log_dw_amt = ifelse(
dw_crisis == 1 & dw_crisis_amt > 0, log(dw_crisis_amt), NA_real_
)
)
# ==============================================================================
# ARBITRAGE PERIOD
# ==============================================================================
df_arb <- df_2023q3 %>%
left_join(btfp_arb %>% select(idrssd, btfp_arb, btfp_arb_amt), by = "idrssd") %>%
left_join(dw_arb %>% select(idrssd, dw_arb), by = "idrssd") %>%
mutate(
btfp_arb = replace_na(btfp_arb, 0L),
dw_arb = replace_na(dw_arb, 0L),
fhlb_user = as.integer(abnormal_fhlb_borrowing_10pct == 1),
any_fed = as.integer(btfp_arb == 1 | dw_arb == 1),
both_fed = as.integer(btfp_arb == 1 & dw_arb == 1),
non_user = as.integer(any_fed == 0 & fhlb_user == 0),
user_group = factor(
case_when(
both_fed == 1 ~ "Both",
btfp_arb == 1 ~ "BTFP_Only",
dw_arb == 1 ~ "DW_Only",
TRUE ~ "Neither"
),
levels = c("Neither", "BTFP_Only", "DW_Only", "Both")
)
)
cat("=== PERIOD DATASET COUNTS ===\n")
## === PERIOD DATASET COUNTS ===
cat("Crisis: ", nrow(df_crisis),
" | BTFP=1:", sum(df_crisis$btfp_crisis),
" | DW=1:", sum(df_crisis$dw_crisis),
" | Both:", sum(df_crisis$both_fed),
" | AnyFed:", sum(df_crisis$any_fed), "\n")
## Crisis: 4292 | BTFP=1: 501 | DW=1: 433 | Both: 106 | AnyFed: 828
cat("Arb: ", nrow(df_arb),
" | BTFP=1:", sum(df_arb$btfp_arb),
" | DW=1:", sum(df_arb$dw_arb), "\n")
## Arb: 4214 | BTFP=1: 749 | DW=1: 362
REGRESSION SAMPLES
# ==============================================================================
# PURE COMPARISON SAMPLES (Borrower vs. Pure Non-Borrower)
# ==============================================================================
# --- Crisis Period ---
df_btfp_s <- df_crisis %>% filter(btfp_crisis == 1 | non_user == 1)
df_dw_s <- df_crisis %>% filter(dw_crisis == 1 | non_user == 1)
df_anyfed_s <- df_crisis %>% filter(any_fed == 1 | non_user == 1)
df_fhlb_s <- df_crisis %>% filter(fhlb_user == 1 | non_user == 1)
# --- DSSW Solvency Splits: Crisis (for Model 1) ---
df_dssw_btfp_sol <- df_btfp_s %>% filter(dssw_solvent == 1)
df_dssw_btfp_ins <- df_btfp_s %>% filter(dssw_insolvent == 1)
df_dssw_dw_sol <- df_dw_s %>% filter(dssw_solvent == 1)
df_dssw_dw_ins <- df_dw_s %>% filter(dssw_insolvent == 1)
df_dssw_any_sol <- df_anyfed_s %>% filter(dssw_solvent == 1)
df_dssw_any_ins <- df_anyfed_s %>% filter(dssw_insolvent == 1)
df_dssw_fhlb_sol <- df_fhlb_s %>% filter(dssw_solvent == 1)
df_dssw_fhlb_ins <- df_fhlb_s %>% filter(dssw_insolvent == 1)
# --- Arbitrage Period (for Model 2) ---
df_btfp_arb_s <- df_arb %>% filter(btfp_arb == 1 | non_user == 1)
df_dw_arb_s <- df_arb %>% filter(dw_arb == 1 | non_user == 1)
df_dssw_btfp_arb_sol <- df_btfp_arb_s %>% filter(dssw_solvent == 1)
df_dssw_btfp_arb_ins <- df_btfp_arb_s %>% filter(dssw_insolvent == 1)
df_dssw_dw_arb_sol <- df_dw_arb_s %>% filter(dssw_solvent == 1)
df_dssw_dw_arb_ins <- df_dw_arb_s %>% filter(dssw_insolvent == 1)
# --- Model 3: Deposit Beta samples (need non-missing uninsured_beta) ---
df_beta_btfp_s <- df_btfp_s %>% filter(!is.na(uninsured_beta_w))
df_beta_dw_s <- df_dw_s %>% filter(!is.na(uninsured_beta_w))
df_beta_anyfed_s <- df_anyfed_s %>% filter(!is.na(uninsured_beta_w))
df_beta_fhlb_s <- df_fhlb_s %>% filter(!is.na(uninsured_beta_w))
# --- Model 4: Facility Choice samples (Crisis emergency borrowers only) ---
# 4a. Multinomial: Among all emergency borrowers
df_emerg_borrowers <- df_crisis %>%
filter(any_fed == 1) %>%
mutate(
facility_choice = factor(
case_when(
both_fed == 1 ~ "Both",
btfp_crisis == 1 ~ "BTFP_Only",
dw_crisis == 1 ~ "DW_Only"
),
levels = c("DW_Only", "BTFP_Only", "Both")
)
)
# 4b. Complementarity: Among DW borrowers, did they ALSO use BTFP?
df_dw_complement <- df_crisis %>%
filter(dw_crisis == 1) %>%
mutate(also_btfp = as.integer(btfp_crisis == 1))
# 4c. Substitution: BTFP-only vs. DW-only
df_substitution <- df_crisis %>%
filter(user_group %in% c("BTFP_Only", "DW_Only")) %>%
mutate(chose_btfp = as.integer(user_group == "BTFP_Only"))
cat("=== REGRESSION SAMPLES ===\n")
## === REGRESSION SAMPLES ===
cat("Model 1 (Crisis DSSW splits):\n")
## Model 1 (Crisis DSSW splits):
cat(" BTFP — Sol:", nrow(df_dssw_btfp_sol), "| Ins:", nrow(df_dssw_btfp_ins), "\n")
## BTFP — Sol: 3665 | Ins: 0
cat(" DW — Sol:", nrow(df_dssw_dw_sol), "| Ins:", nrow(df_dssw_dw_ins), "\n")
## DW — Sol: 3599 | Ins: 0
cat(" Any — Sol:", nrow(df_dssw_any_sol), "| Ins:", nrow(df_dssw_any_ins), "\n")
## Any — Sol: 3991 | Ins: 0
cat(" FHLB — Sol:", nrow(df_dssw_fhlb_sol), "| Ins:", nrow(df_dssw_fhlb_ins), "\n")
## FHLB — Sol: 3468 | Ins: 0
cat("\nModel 2 (Arb DSSW splits):\n")
##
## Model 2 (Arb DSSW splits):
cat(" BTFP Arb — Sol:", nrow(df_dssw_btfp_arb_sol), "| Ins:", nrow(df_dssw_btfp_arb_ins), "\n")
## BTFP Arb — Sol: 3748 | Ins: 0
cat(" DW Arb — Sol:", nrow(df_dssw_dw_arb_sol), "| Ins:", nrow(df_dssw_dw_arb_ins), "\n")
## DW Arb — Sol: 3364 | Ins: 0
cat("\nModel 3 (Beta coverage):\n")
##
## Model 3 (Beta coverage):
cat(" BTFP:", nrow(df_beta_btfp_s), "| DW:", nrow(df_beta_dw_s),
"| Any:", nrow(df_beta_anyfed_s), "| FHLB:", nrow(df_beta_fhlb_s), "\n")
## BTFP: 3665 | DW: 3599 | Any: 3991 | FHLB: 3468
cat("\nModel 4 (Facility choice):\n")
##
## Model 4 (Facility choice):
cat(" Emergency borrowers:", nrow(df_emerg_borrowers), "\n")
## Emergency borrowers: 828
cat(" DW borrowers (complement test):", nrow(df_dw_complement), "\n")
## DW borrowers (complement test): 433
cat(" BTFP-only vs DW-only (substitution):", nrow(df_substitution), "\n")
## BTFP-only vs DW-only (substitution): 722
MODEL
INFRASTRUCTURE
# ==============================================================================
# CONTROLS
# ==============================================================================
CONTROLS <- "ln_assets + cash_ratio + loan_to_deposit + book_equity_ratio + wholesale + roa"
# ==============================================================================
# FORMULA STRINGS
# ==============================================================================
# Model 1: Base specification (same for all 4 models, varies by sample)
EXPL_BASE <- "mtm_total + uninsured_lev + mtm_x_uninsured"
# Model 3: Deposit beta channel (triple interaction)
EXPL_BETA_FULL <- paste0(
"mtm_total + uninsured_lev + uninsured_beta + ",
"mtm_x_uninsured + mtm_x_unins_beta + unins_x_unins_beta + ",
"mtm_x_unins_x_beta"
)
# Model 3 nested: without triple (for comparison)
EXPL_BETA_NOINT <- paste0(
"mtm_total + uninsured_lev + uninsured_beta + ",
"mtm_x_uninsured + mtm_x_unins_beta + unins_x_unins_beta"
)
# Model 4: Facility choice — par benefit + collateral capacity
EXPL_FACILITY <- "mtm_total + uninsured_lev + mtm_x_uninsured + par_benefit + collateral_capacity"
# ==============================================================================
# FIXEST VARIABLE DICTIONARY
# ==============================================================================
setFixest_dict(c(
mtm_total = "MTM Loss (z)",
uninsured_lev = "Uninsured Leverage (z)",
insured_lev = "Insured Leverage (z)",
adjusted_equity_dssw = "DSSW Adj.\\ Equity (z)",
dfv = "Deposit Franchise Value (z)",
dssw_ae_x_uninsured = "DSSW AE $\\times$ Uninsured",
uninsured_beta = "Uninsured $\\beta$ (z)",
mtm_x_uninsured = "MTM $\\times$ Uninsured",
mtm_x_insured = "MTM $\\times$ Insured",
mtm_x_unins_beta = "MTM $\\times$ Unins. $\\beta$",
unins_x_unins_beta = "Uninsured $\\times$ Unins. $\\beta$",
mtm_x_unins_x_beta = "MTM $\\times$ Uninsured $\\times$ Unins. $\\beta$",
par_benefit = "Par Benefit (z)",
collateral_capacity = "OMO Collateral (z)",
par_x_uninsured = "Par $\\times$ Uninsured",
ln_assets = "Log(Assets)",
cash_ratio = "Cash Ratio",
loan_to_deposit = "Loan-to-Deposit",
book_equity_ratio = "Book Equity Ratio",
wholesale = "Wholesale Funding",
roa = "ROA"
))
COEF_ORDER <- c(
"Constant",
"MTM Loss",
"Uninsured Leverage",
"Uninsured.*beta",
"MTM.*Uninsured",
"MTM.*Unins.*beta",
"Uninsured.*Unins.*beta",
"MTM.*Uninsured.*Unins",
"Par Benefit",
"OMO Collateral",
"DSSW Adj.*Equity",
"Deposit Franchise"
)
# ==============================================================================
# GENERIC MODEL RUNNER
# ==============================================================================
run_model <- function(data, dv, explanatory,
family_type = "lpm", controls = CONTROLS) {
ff <- as.formula(paste(dv, "~", explanatory, "+", controls))
if (family_type == "lpm") {
feols(ff, data = data, vcov = "hetero")
} else if (family_type == "logit") {
feglm(ff, data = data, family = binomial("logit"), vcov = "hetero")
} else {
stop("family_type must be either 'lpm' or 'logit'")
}
}
# ==============================================================================
# SAFE MODEL RUNNER (catches errors for small samples)
# ==============================================================================
safe_run_model <- function(data, dv, explanatory, family_type = "lpm",
controls = CONTROLS, min_n = 30) {
n_obs <- sum(!is.na(data[[dv]]))
n_dv1 <- sum(data[[dv]] == 1, na.rm = TRUE)
if (n_obs < min_n || n_dv1 < 5) {
message(" ⚠ Skipping: N=", n_obs, ", DV=1 count=", n_dv1,
" (below threshold for ", dv, ")")
return(NULL)
}
tryCatch(
run_model(data, dv, explanatory, family_type, controls),
error = function(e) {
message(" ⚠ Model failed for ", dv, ": ", e$message)
return(NULL)
}
)
}
# ==============================================================================
# ETABLE SAVER
# ==============================================================================
save_etable <- function(models, filename, title_text, notes_text,
fitstat_use = ~ n + r2,
extra_lines = NULL) {
# Remove NULLs from model list
models <- models[!sapply(models, is.null)]
if (length(models) == 0) {
message(" ⚠ No valid models for ", filename, ". Skipping save.")
return(invisible(NULL))
}
etable(
models,
title = title_text,
notes = notes_text,
fitstat = fitstat_use,
order = COEF_ORDER,
extralines = extra_lines,
se.below = TRUE,
tex = TRUE,
file = file.path(TABLE_PATH, paste0(filename, ".tex")),
replace = TRUE,
style.tex = style.tex("aer")
)
message("Saved: ", filename, ".tex")
}
# ==============================================================================
# THEME
# ==============================================================================
theme_paper <- theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 13, hjust = 0),
plot.subtitle = element_text(size = 10, color = "grey40", hjust = 0),
legend.position = "bottom",
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold", size = 11)
)
pal_user <- c(
"DW" = "#D62828",
"BTFP" = "#003049",
"FHLB" = "#F77F00",
"Pure Non-Borrower" = "grey70",
"Both" = "#7209B7",
"AnyFed" = "#2A9D8F"
)
cat("=== MODEL INFRASTRUCTURE READY ===\n")
## === MODEL INFRASTRUCTURE READY ===
MODEL
1: Main Results — DSSW Solvency Split (Crisis Period)
8 Regressions:
Solvent vs. Insolvent × 4 Borrower Types
# ==============================================================================
# MODEL 1: EXTENSIVE MARGIN — CRISIS PERIOD
# 8 regressions: {AnyFed, BTFP, DW, FHLB} × {DSSW Solvent, DSSW Insolvent}
#
# Specification: Borrower = MTM + Uninsured + (MTM × Uninsured) + Controls
# Solvency: DSSW Adjusted Equity = Book Equity/TA - MTM/TA + DFV/TA ≥ 0
# ==============================================================================
cat("=== MODEL 1: MAIN RESULTS (CRISIS, DSSW SOLVENCY) ===\n\n")
## === MODEL 1: MAIN RESULTS (CRISIS, DSSW SOLVENCY) ===
# --- AnyFed ---
mod1_any_sol <- safe_run_model(df_dssw_any_sol, "any_fed", EXPL_BASE)
mod1_any_ins <- safe_run_model(df_dssw_any_ins, "any_fed", EXPL_BASE)
# --- BTFP ---
mod1_btfp_sol <- safe_run_model(df_dssw_btfp_sol, "btfp_crisis", EXPL_BASE)
mod1_btfp_ins <- safe_run_model(df_dssw_btfp_ins, "btfp_crisis", EXPL_BASE)
# --- DW ---
mod1_dw_sol <- safe_run_model(df_dssw_dw_sol, "dw_crisis", EXPL_BASE)
mod1_dw_ins <- safe_run_model(df_dssw_dw_ins, "dw_crisis", EXPL_BASE)
# --- FHLB ---
mod1_fhlb_sol <- safe_run_model(df_dssw_fhlb_sol, "fhlb_user", EXPL_BASE)
mod1_fhlb_ins <- safe_run_model(df_dssw_fhlb_ins, "fhlb_user", EXPL_BASE)
models_m1 <- list(
"Any (Sol)" = mod1_any_sol,
"Any (Ins)" = mod1_any_ins,
"BTFP (Sol)" = mod1_btfp_sol,
"BTFP (Ins)" = mod1_btfp_ins,
"DW (Sol)" = mod1_dw_sol,
"DW (Ins)" = mod1_dw_ins,
"FHLB (Sol)" = mod1_fhlb_sol,
"FHLB (Ins)" = mod1_fhlb_ins
)
# DV means
calc_dv_mean <- function(df, dv) round(mean(df[[dv]], na.rm = TRUE), 3)
save_etable(
models = models_m1,
filename = "M1_main_dssw_solvency_crisis",
title_text = "Model 1: Emergency Facility Selection by DSSW Solvency (Crisis Period)",
notes_text = paste0(
"LPM. DV = 1 if bank borrowed from the facility during the Crisis Period ",
"(Mar 8--May 4, 2023), 0 if pure non-borrower. ",
"Solvency: DSSW Adjusted Equity = Book Equity/TA $-$ MTM Loss/TA ",
"$+$ DFV/TA, where DFV $= (1 - \\beta_i) \\times$ Deposits/TA ",
"following Drechsler, Savov, Schnabl \\& Wang (2023/2025). ",
"Solvent: DSSW AE $\\geq 0$; Insolvent: DSSW AE $< 0$. ",
"Robust SEs. z-standardized regressors."
),
extra_lines = list(
c("Solvency", "Solvent", "Insolvent", "Solvent", "Insolvent",
"Solvent", "Insolvent", "Solvent", "Insolvent")
)
)
cat("=== MODEL 1 RESULTS ===\n")
## === MODEL 1 RESULTS ===
etable(models_m1[!sapply(models_m1, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## Any (Sol) BTFP (Sol) DW (Sol) FHLB (Sol)
## Dependent Var.: any_fed btfp_crisis dw_crisis fhlb_user
##
## Constant 0.2071*** 0.1423*** 0.1267*** 0.0897***
## (0.0061) (0.0056) (0.0054) (0.0050)
## MTM Loss (z) 0.0295*** 0.0243*** 0.0151* 0.0042
## (0.0070) (0.0062) (0.0061) (0.0056)
## Uninsured Leverage (z) 0.0199** 0.0219** 0.0119 0.0065
## (0.0077) (0.0071) (0.0068) (0.0052)
## MTM $\times$ Uninsured 0.0189** 0.0181*** 0.0165** -0.0052
## (0.0059) (0.0054) (0.0052) (0.0045)
## Log(Assets) 0.1078*** 0.0724*** 0.0934*** 0.0277***
## (0.0083) (0.0078) (0.0078) (0.0065)
## Cash Ratio -0.0241*** -0.0283*** -0.0036 -0.0207***
## (0.0063) (0.0050) (0.0055) (0.0040)
## Loan-to-Deposit -0.0016 -0.0085 2.01e-5 0.0256***
## (0.0071) (0.0062) (0.0061) (0.0054)
## Book Equity Ratio -0.0150* -0.0136** -0.0012 0.0082
## (0.0059) (0.0051) (0.0048) (0.0045)
## Wholesale Funding 0.0295*** 0.0265*** 0.0202** 0.0024
## (0.0069) (0.0066) (0.0064) (0.0052)
## ROA -0.0033 -0.0075 -0.0033 -0.0103*
## (0.0060) (0.0052) (0.0051) (0.0045)
## ______________________ __________ ___________ __________ __________
## S.E. type Hete.-rob. Heter.-rob. Hete.-rob. Hete.-rob.
## Observations 3,991 3,665 3,599 3,468
## R2 0.11987 0.09589 0.10258 0.04088
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
MODEL
2: BTFP & DW — Crisis vs. Arbitrage (DSSW Solvency Split)
Crisis Period
# ==============================================================================
# MODEL 2a: CRISIS PERIOD — BTFP and DW × {Solvent, Insolvent}
# ==============================================================================
cat("=== MODEL 2: BTFP & DW TEMPORAL (DSSW SOLVENCY) ===\n\n")
## === MODEL 2: BTFP & DW TEMPORAL (DSSW SOLVENCY) ===
# BTFP Crisis (already built above, reuse)
mod2_btfp_cr_sol <- mod1_btfp_sol
mod2_btfp_cr_ins <- mod1_btfp_ins
# DW Crisis (already built above, reuse)
mod2_dw_cr_sol <- mod1_dw_sol
mod2_dw_cr_ins <- mod1_dw_ins
models_m2_crisis <- list(
"BTFP Crisis (Sol)" = mod2_btfp_cr_sol,
"BTFP Crisis (Ins)" = mod2_btfp_cr_ins,
"DW Crisis (Sol)" = mod2_dw_cr_sol,
"DW Crisis (Ins)" = mod2_dw_cr_ins
)
save_etable(
models = models_m2_crisis,
filename = "M2a_btfp_dw_crisis_solvency",
title_text = "Model 2a: BTFP \\& DW Selection by DSSW Solvency (Crisis Period)",
notes_text = paste0(
"LPM. DV = 1 if bank borrowed from the respective facility during the Crisis Period ",
"(Mar 8--May 4, 2023). Solvency defined by DSSW Adjusted Equity. ",
"Robust SEs. z-standardized regressors."
),
extra_lines = list(
c("Facility", "BTFP", "BTFP", "DW", "DW"),
c("Solvency", "Solvent", "Insolvent", "Solvent", "Insolvent")
)
)
cat("=== MODEL 2a: CRISIS ===\n")
## === MODEL 2a: CRISIS ===
etable(models_m2_crisis[!sapply(models_m2_crisis, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## BTFP Cris.. DW Crisi..
## Dependent Var.: btfp_crisis dw_crisis
##
## Constant 0.1423*** 0.1267***
## (0.0056) (0.0054)
## MTM Loss (z) 0.0243*** 0.0151*
## (0.0062) (0.0061)
## Uninsured Leverage (z) 0.0219** 0.0119
## (0.0071) (0.0068)
## MTM $\times$ Uninsured 0.0181*** 0.0165**
## (0.0054) (0.0052)
## Log(Assets) 0.0724*** 0.0934***
## (0.0078) (0.0078)
## Cash Ratio -0.0283*** -0.0036
## (0.0050) (0.0055)
## Loan-to-Deposit -0.0085 2.01e-5
## (0.0062) (0.0061)
## Book Equity Ratio -0.0136** -0.0012
## (0.0051) (0.0048)
## Wholesale Funding 0.0265*** 0.0202**
## (0.0066) (0.0064)
## ROA -0.0075 -0.0033
## (0.0052) (0.0051)
## ______________________ ___________ __________
## S.E. type Heter.-rob. Hete.-rob.
## Observations 3,665 3,599
## R2 0.09589 0.10258
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Arbitrage
Period
# ==============================================================================
# MODEL 2b: ARBITRAGE PERIOD — BTFP and DW × {Solvent, Insolvent}
# Note: DW data extends to 2023-12-31, arbitrage starts Nov 15.
# DW arbitrage sample may be small.
# ==============================================================================
mod2_btfp_arb_sol <- safe_run_model(df_dssw_btfp_arb_sol, "btfp_arb", EXPL_BASE)
mod2_btfp_arb_ins <- safe_run_model(df_dssw_btfp_arb_ins, "btfp_arb", EXPL_BASE)
mod2_dw_arb_sol <- safe_run_model(df_dssw_dw_arb_sol, "dw_arb", EXPL_BASE)
mod2_dw_arb_ins <- safe_run_model(df_dssw_dw_arb_ins, "dw_arb", EXPL_BASE)
models_m2_arb <- list(
"BTFP Arb (Sol)" = mod2_btfp_arb_sol,
"BTFP Arb (Ins)" = mod2_btfp_arb_ins,
"DW Arb (Sol)" = mod2_dw_arb_sol,
"DW Arb (Ins)" = mod2_dw_arb_ins
)
save_etable(
models = models_m2_arb,
filename = "M2b_btfp_dw_arb_solvency",
title_text = "Model 2b: BTFP \\& DW Selection by DSSW Solvency (Arbitrage Period)",
notes_text = paste0(
"LPM. DV = 1 if bank borrowed during the Arbitrage Period ",
"(Nov 15, 2023--Jan 24, 2024). DW data available through Dec 31, 2023. ",
"Solvency defined by DSSW Adjusted Equity (2023Q3 baseline for BTFP, 2022Q4 betas). ",
"Prediction: MTM $\\times$ Uninsured should be weak/insignificant in arbitrage. ",
"Robust SEs. z-standardized regressors."
),
extra_lines = list(
c("Facility", "BTFP", "BTFP", "DW", "DW"),
c("Solvency", "Solvent", "Insolvent", "Solvent", "Insolvent")
)
)
cat("=== MODEL 2b: ARBITRAGE ===\n")
## === MODEL 2b: ARBITRAGE ===
etable(models_m2_arb[!sapply(models_m2_arb, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## BTFP Arb.. DW Arb (..
## Dependent Var.: btfp_arb dw_arb
##
## Constant 0.2006*** 0.1165***
## (0.0060) (0.0056)
## MTM Loss (z) 0.0227** -0.0036
## (0.0072) (0.0065)
## Uninsured Leverage (z) 0.0124 -0.0094
## (0.0071) (0.0064)
## MTM $\times$ Uninsured 0.0054 0.0007
## (0.0057) (0.0054)
## Log(Assets) 0.0650*** 0.0780***
## (0.0078) (0.0075)
## Cash Ratio -0.0365*** -0.0098
## (0.0061) (0.0053)
## Loan-to-Deposit -0.0004 -0.0002
## (0.0068) (0.0055)
## Book Equity Ratio -0.0089 -0.0063
## (0.0061) (0.0049)
## Wholesale Funding 0.1089*** 0.0326***
## (0.0079) (0.0072)
## ROA -0.0265*** -0.0089
## (0.0060) (0.0050)
## ______________________ __________ __________
## S.E. type Hete.-rob. Hete.-rob.
## Observations 3,748 3,364
## R2 0.16390 0.08042
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Combined Table:
Crisis vs. Arbitrage
# ==============================================================================
# MODEL 2 COMBINED: Side-by-side Crisis vs Arbitrage
# ==============================================================================
models_m2_combined <- list(
"BTFP Crisis (Sol)" = mod2_btfp_cr_sol,
"BTFP Crisis (Ins)" = mod2_btfp_cr_ins,
"BTFP Arb (Sol)" = mod2_btfp_arb_sol,
"BTFP Arb (Ins)" = mod2_btfp_arb_ins,
"DW Crisis (Sol)" = mod2_dw_cr_sol,
"DW Crisis (Ins)" = mod2_dw_cr_ins,
"DW Arb (Sol)" = mod2_dw_arb_sol,
"DW Arb (Ins)" = mod2_dw_arb_ins
)
save_etable(
models = models_m2_combined,
filename = "M2_combined_temporal_solvency",
title_text = "Model 2: Crisis vs. Arbitrage by Facility and DSSW Solvency",
notes_text = paste0(
"LPM. Columns 1--4: BTFP; Columns 5--8: DW. ",
"Crisis = Mar 8--May 4, 2023; Arbitrage = Nov 15, 2023--Jan 24, 2024. ",
"Solvency defined by DSSW Adjusted Equity. ",
"Robust SEs. z-standardized regressors."
),
extra_lines = list(
c("Period", "Crisis", "Crisis", "Arb", "Arb",
"Crisis", "Crisis", "Arb", "Arb"),
c("Solvency", "Sol", "Ins", "Sol", "Ins",
"Sol", "Ins", "Sol", "Ins")
)
)
cat("=== MODEL 2 COMBINED TABLE ===\n")
## === MODEL 2 COMBINED TABLE ===
etable(models_m2_combined[!sapply(models_m2_combined, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## BTFP Cris.. BTFP Arb.. DW Crisi.. DW Arb (..
## Dependent Var.: btfp_crisis btfp_arb dw_crisis dw_arb
##
## Constant 0.1423*** 0.2006*** 0.1267*** 0.1165***
## (0.0056) (0.0060) (0.0054) (0.0056)
## MTM Loss (z) 0.0243*** 0.0227** 0.0151* -0.0036
## (0.0062) (0.0072) (0.0061) (0.0065)
## Uninsured Leverage (z) 0.0219** 0.0124 0.0119 -0.0094
## (0.0071) (0.0071) (0.0068) (0.0064)
## MTM $\times$ Uninsured 0.0181*** 0.0054 0.0165** 0.0007
## (0.0054) (0.0057) (0.0052) (0.0054)
## Log(Assets) 0.0724*** 0.0650*** 0.0934*** 0.0780***
## (0.0078) (0.0078) (0.0078) (0.0075)
## Cash Ratio -0.0283*** -0.0365*** -0.0036 -0.0098
## (0.0050) (0.0061) (0.0055) (0.0053)
## Loan-to-Deposit -0.0085 -0.0004 2.01e-5 -0.0002
## (0.0062) (0.0068) (0.0061) (0.0055)
## Book Equity Ratio -0.0136** -0.0089 -0.0012 -0.0063
## (0.0051) (0.0061) (0.0048) (0.0049)
## Wholesale Funding 0.0265*** 0.1089*** 0.0202** 0.0326***
## (0.0066) (0.0079) (0.0064) (0.0072)
## ROA -0.0075 -0.0265*** -0.0033 -0.0089
## (0.0052) (0.0060) (0.0051) (0.0050)
## ______________________ ___________ __________ __________ __________
## S.E. type Heter.-rob. Hete.-rob. Hete.-rob. Hete.-rob.
## Observations 3,665 3,748 3,599 3,364
## R2 0.09589 0.16390 0.10258 0.08042
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
MODEL
3: Deposit Beta Channel — Triple Interaction
Main
Specification
# ==============================================================================
# MODEL 3: DEPOSIT BETA ON UNINSURED
#
# Introduces uninsured_beta (β^U from DSSW estimation) and tests whether the
# panic mechanism (MTM × Uninsured) is amplified for banks whose uninsured
# deposits are more rate-sensitive (high β^U).
#
# Full specification:
# Borrower = MTM + Uninsured + Unins_Beta + (MTM × Uninsured)
# + (MTM × Unins_Beta) + (Uninsured × Unins_Beta)
# + (MTM × Uninsured × Unins_Beta) + Controls
#
# Key prediction: β_7 (triple interaction) > 0
# → Banks with higher uninsured beta (flightier deposits) see a stronger
# panic amplification from MTM × Uninsured.
# ==============================================================================
cat("=== MODEL 3: DEPOSIT BETA CHANNEL ===\n\n")
## === MODEL 3: DEPOSIT BETA CHANNEL ===
# --- AnyFed ---
mod3_any_full <- safe_run_model(df_beta_anyfed_s, "any_fed", EXPL_BETA_FULL)
mod3_any_noint <- safe_run_model(df_beta_anyfed_s, "any_fed", EXPL_BETA_NOINT)
# --- BTFP ---
mod3_btfp_full <- safe_run_model(df_beta_btfp_s, "btfp_crisis", EXPL_BETA_FULL)
mod3_btfp_noint <- safe_run_model(df_beta_btfp_s, "btfp_crisis", EXPL_BETA_NOINT)
# --- DW ---
mod3_dw_full <- safe_run_model(df_beta_dw_s, "dw_crisis", EXPL_BETA_FULL)
mod3_dw_noint <- safe_run_model(df_beta_dw_s, "dw_crisis", EXPL_BETA_NOINT)
# --- FHLB (falsification) ---
mod3_fhlb_full <- safe_run_model(df_beta_fhlb_s, "fhlb_user", EXPL_BETA_FULL)
models_m3 <- list(
"Any (no triple)" = mod3_any_noint,
"Any (full)" = mod3_any_full,
"BTFP (no triple)" = mod3_btfp_noint,
"BTFP (full)" = mod3_btfp_full,
"DW (no triple)" = mod3_dw_noint,
"DW (full)" = mod3_dw_full,
"FHLB (full)" = mod3_fhlb_full
)
save_etable(
models = models_m3,
filename = "M3_deposit_beta_channel",
title_text = "Model 3: Deposit Beta Channel — Triple Interaction",
notes_text = paste0(
"LPM. DV = 1 if bank borrowed during Crisis Period, 0 if pure non-borrower. ",
"Uninsured $\\beta$ is the bank-specific deposit beta on uninsured deposits ",
"estimated following DSSW (2017/2021). The triple interaction ",
"MTM $\\times$ Uninsured $\\times$ Unins.\\\\ $\\beta$ tests whether the ",
"panic mechanism is stronger for banks with flightier (higher-$\\beta$) ",
"uninsured deposits. FHLB serves as falsification. ",
"Robust SEs. z-standardized regressors."
),
extra_lines = list(
c("Triple interaction", "No", "Yes", "No", "Yes", "No", "Yes", "Yes")
)
)
cat("=== MODEL 3 RESULTS ===\n")
## === MODEL 3 RESULTS ===
etable(models_m3[!sapply(models_m3, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## Any (no .. Any (full)
## Dependent Var.: any_fed any_fed
##
## Constant 0.2077*** 0.2075***
## (0.0061) (0.0062)
## MTM Loss (z) 0.0302*** 0.0292***
## (0.0071) (0.0072)
## Uninsured Leverage (z) 0.0208** 0.0192*
## (0.0077) (0.0078)
## Uninsured $\beta$ (z) 0.0067 0.0063
## (0.0073) (0.0073)
## MTM $\times$ Uninsured 0.0190** 0.0190**
## (0.0059) (0.0059)
## MTM $\times$ Unins. $\beta$ 0.0067 0.0070
## (0.0062) (0.0062)
## Uninsured $\times$ Unins. $\beta$ -0.0041 -0.0089
## (0.0059) (0.0068)
## Log(Assets) 0.1072*** 0.1073***
## (0.0084) (0.0084)
## Cash Ratio -0.0245*** -0.0243***
## (0.0064) (0.0064)
## Loan-to-Deposit -0.0030 -0.0034
## (0.0073) (0.0073)
## Book Equity Ratio -0.0145* -0.0143*
## (0.0059) (0.0060)
## Wholesale Funding 0.0297*** 0.0302***
## (0.0069) (0.0069)
## ROA -0.0032 -0.0030
## (0.0060) (0.0060)
## MTM $\times$ Uninsured $\times$ Unins. $\beta$ -0.0099
## (0.0057)
## ________________________________________ __________ __________
## S.E. type Hete.-rob. Hete.-rob.
## Observations 3,991 3,991
## R2 0.12038 0.12113
##
## BTFP (no .. BTFP (full)
## Dependent Var.: btfp_crisis btfp_crisis
##
## Constant 0.1435*** 0.1433***
## (0.0057) (0.0057)
## MTM Loss (z) 0.0255*** 0.0250***
## (0.0063) (0.0063)
## Uninsured Leverage (z) 0.0230** 0.0223**
## (0.0071) (0.0072)
## Uninsured $\beta$ (z) 0.0101 0.0099
## (0.0067) (0.0067)
## MTM $\times$ Uninsured 0.0188*** 0.0187***
## (0.0055) (0.0055)
## MTM $\times$ Unins. $\beta$ 0.0102 0.0101
## (0.0056) (0.0056)
## Uninsured $\times$ Unins. $\beta$ -0.0033 -0.0056
## (0.0054) (0.0066)
## Log(Assets) 0.0714*** 0.0715***
## (0.0079) (0.0079)
## Cash Ratio -0.0289*** -0.0287***
## (0.0051) (0.0051)
## Loan-to-Deposit -0.0108 -0.0110
## (0.0062) (0.0062)
## Book Equity Ratio -0.0128* -0.0127*
## (0.0051) (0.0051)
## Wholesale Funding 0.0266*** 0.0269***
## (0.0066) (0.0066)
## ROA -0.0070 -0.0070
## (0.0052) (0.0052)
## MTM $\times$ Uninsured $\times$ Unins. $\beta$ -0.0047
## (0.0053)
## ________________________________________ ___________ ___________
## S.E. type Heter.-rob. Heter.-rob.
## Observations 3,665 3,665
## R2 0.09725 0.09748
##
## DW (no t.. DW (full) FHLB (fu..
## Dependent Var.: dw_crisis dw_crisis fhlb_user
##
## Constant 0.1270*** 0.1268*** 0.0903***
## (0.0055) (0.0055) (0.0051)
## MTM Loss (z) 0.0153* 0.0145* 0.0065
## (0.0062) (0.0062) (0.0057)
## Uninsured Leverage (z) 0.0120 0.0105 0.0082
## (0.0068) (0.0068) (0.0054)
## Uninsured $\beta$ (z) 0.0004 -0.0002 0.0154**
## (0.0064) (0.0064) (0.0060)
## MTM $\times$ Uninsured 0.0169** 0.0168** -0.0045
## (0.0052) (0.0052) (0.0045)
## MTM $\times$ Unins. $\beta$ 0.0017 0.0018 0.0033
## (0.0055) (0.0054) (0.0048)
## Uninsured $\times$ Unins. $\beta$ 0.0019 -0.0030 0.0021
## (0.0053) (0.0064) (0.0055)
## Log(Assets) 0.0935*** 0.0935*** 0.0245***
## (0.0079) (0.0079) (0.0066)
## Cash Ratio -0.0036 -0.0034 -0.0195***
## (0.0055) (0.0055) (0.0041)
## Loan-to-Deposit -0.0001 -0.0003 0.0215***
## (0.0063) (0.0063) (0.0054)
## Book Equity Ratio -0.0010 -0.0008 0.0087
## (0.0048) (0.0048) (0.0045)
## Wholesale Funding 0.0202** 0.0207** 0.0032
## (0.0064) (0.0063) (0.0052)
## ROA -0.0032 -0.0032 -0.0092*
## (0.0052) (0.0052) (0.0045)
## MTM $\times$ Uninsured $\times$ Unins. $\beta$ -0.0093 -0.0028
## (0.0051) (0.0047)
## ________________________________________ __________ __________ __________
## S.E. type Hete.-rob. Hete.-rob. Hete.-rob.
## Observations 3,599 3,599 3,468
## R2 0.10264 0.10362 0.04378
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Uninsured Beta
Diagnostics
# ==============================================================================
# DIAGNOSTICS: Distribution of uninsured beta and its correlates
# ==============================================================================
cat("=== UNINSURED BETA DIAGNOSTICS (2022Q4) ===\n\n")
## === UNINSURED BETA DIAGNOSTICS (2022Q4) ===
cat("--- Coverage ---\n")
## --- Coverage ---
n_total <- nrow(df_2022q4)
n_ubeta <- sum(!is.na(df_2022q4$uninsured_beta_raw))
cat(sprintf("Uninsured beta coverage: %d / %d banks (%.1f%%)\n",
n_ubeta, n_total, 100 * n_ubeta / n_total))
## Uninsured beta coverage: 4226 / 4292 banks (98.5%)
cat("\n--- Distribution (raw) ---\n")
##
## --- Distribution (raw) ---
print(summary(df_2022q4$uninsured_beta_raw))
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 0.2640 0.3337 0.3895 0.4111 0.4683 0.6692 66
cat("\n--- Correlation with key variables ---\n")
##
## --- Correlation with key variables ---
cor_vars <- df_2022q4 %>%
select(uninsured_beta_raw, beta_overall,
uninsured_lev_raw, mtm_total_raw,
adjusted_equity_dssw_raw, ln_assets_raw) %>%
drop_na()
if (nrow(cor_vars) > 10) {
print(round(cor(cor_vars), 3))
}
## uninsured_beta_raw beta_overall uninsured_lev_raw
## uninsured_beta_raw 1.000 0.873 -0.036
## beta_overall 0.873 1.000 0.298
## uninsured_lev_raw -0.036 0.298 1.000
## mtm_total_raw -0.114 -0.190 -0.155
## adjusted_equity_dssw_raw -0.825 -0.894 -0.237
## ln_assets_raw 0.223 0.390 0.424
## mtm_total_raw adjusted_equity_dssw_raw ln_assets_raw
## uninsured_beta_raw -0.114 -0.825 0.223
## beta_overall -0.190 -0.894 0.390
## uninsured_lev_raw -0.155 -0.237 0.424
## mtm_total_raw 1.000 -0.036 0.031
## adjusted_equity_dssw_raw -0.036 1.000 -0.451
## ln_assets_raw 0.031 -0.451 1.000
cat("\n--- Mean Uninsured Beta by DSSW Solvency ---\n")
##
## --- Mean Uninsured Beta by DSSW Solvency ---
df_2022q4 %>%
filter(!is.na(uninsured_beta_raw), !is.na(dssw_solvent)) %>%
group_by(DSSW_Solvent = dssw_solvent) %>%
summarise(
N = n(),
Mean_Unins_Beta = round(mean(uninsured_beta_raw, na.rm = TRUE), 4),
SD_Unins_Beta = round(sd(uninsured_beta_raw, na.rm = TRUE), 4),
.groups = "drop"
) %>%
print()
## # A tibble: 1 × 4
## DSSW_Solvent N Mean_Unins_Beta SD_Unins_Beta
## <int> <int> <dbl> <dbl>
## 1 1 4226 0.411 0.105
MODEL
4: BTFP Complementarity with DW
4a. Multinomial
Facility Choice
# ==============================================================================
# MODEL 4a: MULTINOMIAL LOGIT — FACILITY CHOICE
#
# Among all emergency borrowers (AnyFed == 1) during Crisis:
# Choice ∈ {DW_Only (base), BTFP_Only, Both}
#
# Key insight: All BTFP borrowers are DW-eligible (both are Fed facilities
# for depository institutions). BTFP complements DW through:
# (a) Par valuation of OMO-eligible collateral (removing fire-sale pressure)
# (b) Longer term (up to 1 year vs. typically overnight for DW)
# (c) Less stigma (new program, Board encouraged use)
#
# Predictions:
# - Banks with larger OMO MTM losses → prefer BTFP (higher par subsidy)
# - Banks with broader (non-OMO) collateral → prefer DW (wider collateral)
# - Banks with both → use Both facilities (complement)
# ==============================================================================
cat("=== MODEL 4a: MULTINOMIAL FACILITY CHOICE ===\n\n")
## === MODEL 4a: MULTINOMIAL FACILITY CHOICE ===
cat("Facility choice distribution among emergency borrowers (Crisis):\n")
## Facility choice distribution among emergency borrowers (Crisis):
print(table(df_emerg_borrowers$facility_choice))
##
## DW_Only BTFP_Only Both
## 327 395 106
# Multinomial logit: DW_Only is the reference category
# Include controls + facility-specific variables
ff_mnom <- as.formula(paste0(
"facility_choice ~ mtm_total + uninsured_lev + mtm_x_uninsured + ",
"par_benefit + collateral_capacity + ",
CONTROLS
))
mod4a_mnom <- tryCatch(
multinom(ff_mnom, data = df_emerg_borrowers, trace = FALSE),
error = function(e) {
message(" ⚠ Multinomial logit failed: ", e$message)
NULL
}
)
if (!is.null(mod4a_mnom)) {
cat("\n--- Multinomial Logit Results (Base = DW Only) ---\n")
cat("AIC:", AIC(mod4a_mnom), "\n\n")
# Tidy output
tidy_mnom <- tidy(mod4a_mnom, conf.int = TRUE)
tidy_mnom <- tidy_mnom %>%
mutate(
stars = case_when(
p.value < 0.01 ~ "***",
p.value < 0.05 ~ "**",
p.value < 0.10 ~ "*",
TRUE ~ ""
),
display = sprintf("%.3f%s (%.3f)", estimate, stars, std.error)
)
# Print key coefficients
key_vars <- c("mtm_total", "uninsured_lev", "mtm_x_uninsured",
"par_benefit", "collateral_capacity")
tidy_mnom %>%
filter(term %in% key_vars) %>%
select(y.level, term, display, p.value) %>%
arrange(y.level, term) %>%
print(n = 20)
# Save as LaTeX
latex_mnom <- kable(
tidy_mnom %>%
filter(term %in% key_vars) %>%
select(Outcome = y.level, Variable = term, Estimate = display),
format = "latex", booktabs = TRUE,
caption = "Multinomial Logit: Facility Choice (Base = DW Only)"
)
safe_writeLines(
as.character(latex_mnom),
file.path(TABLE_PATH, "M4a_multinomial_facility_choice.tex")
)
}
##
## --- Multinomial Logit Results (Base = DW Only) ---
## AIC: 1567.913
##
## # A tibble: 10 × 4
## y.level term display p.value
## <chr> <chr> <chr> <dbl>
## 1 BTFP_Only collateral_capacity 0.240** (0.103) 0.0200
## 2 BTFP_Only mtm_total 0.120 (0.103) 0.243
## 3 BTFP_Only mtm_x_uninsured -0.103 (0.086) 0.233
## 4 BTFP_Only par_benefit -0.342*** (0.131) 0.00891
## 5 BTFP_Only uninsured_lev 0.120 (0.093) 0.194
## 6 Both collateral_capacity 0.106 (0.149) 0.476
## 7 Both mtm_total -0.196 (0.172) 0.255
## 8 Both mtm_x_uninsured 0.113 (0.129) 0.381
## 9 Both par_benefit 0.284 (0.380) 0.455
## 10 Both uninsured_lev 0.286** (0.134) 0.0332
4b. Complementarity
Test: DW Borrowers → Also BTFP?
# ==============================================================================
# MODEL 4b: COMPLEMENTARITY TEST
#
# Sample: Banks that borrowed from DW during Crisis
# DV: also_btfp = 1 if bank ALSO borrowed from BTFP (i.e., "Both")
#
# This tests: Among DW borrowers, what drove them to additionally use BTFP?
# Prediction: Banks with larger OMO MTM losses (higher par benefit from BTFP)
# should be more likely to also use BTFP as a complement.
# ==============================================================================
cat("=== MODEL 4b: COMPLEMENTARITY (DW → ALSO BTFP?) ===\n\n")
## === MODEL 4b: COMPLEMENTARITY (DW → ALSO BTFP?) ===
cat("DW borrowers during Crisis:", nrow(df_dw_complement), "\n")
## DW borrowers during Crisis: 433
cat(" Also used BTFP:", sum(df_dw_complement$also_btfp), "\n")
## Also used BTFP: 106
cat(" DW only:", sum(df_dw_complement$also_btfp == 0), "\n\n")
## DW only: 327
# Base specification
mod4b_base <- safe_run_model(
df_dw_complement, "also_btfp", EXPL_BASE, min_n = 20
)
# With par benefit and collateral
mod4b_facility <- safe_run_model(
df_dw_complement, "also_btfp", EXPL_FACILITY, min_n = 20
)
models_m4b <- list(
"Complement (Base)" = mod4b_base,
"Complement (Facility)" = mod4b_facility
)
save_etable(
models = models_m4b,
filename = "M4b_dw_complementarity",
title_text = "Model 4b: BTFP Complementarity — Among DW Borrowers",
notes_text = paste0(
"LPM. Sample: banks that borrowed from the DW during Crisis Period. ",
"DV = 1 if the bank also borrowed from BTFP (Both), 0 if DW-only. ",
"Par Benefit measures the proportional gain from BTFP's par-value lending. ",
"OMO Collateral measures OMO-eligible securities as \\% of assets. ",
"Robust SEs. z-standardized regressors."
)
)
cat("=== MODEL 4b RESULTS ===\n")
## === MODEL 4b RESULTS ===
etable(models_m4b[!sapply(models_m4b, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## Compleme.. Compleme...1
## Dependent Var.: also_btfp also_btfp
##
## Constant 0.1686*** 0.1668***
## (0.0235) (0.0231)
## MTM Loss (z) -0.0219 -0.0186
## (0.0242) (0.0246)
## Uninsured Leverage (z) 0.0490* 0.0499*
## (0.0241) (0.0243)
## MTM $\times$ Uninsured 0.0200 0.0200
## (0.0212) (0.0214)
## Log(Assets) 0.0590* 0.0563*
## (0.0247) (0.0253)
## Cash Ratio -0.0472 -0.0422
## (0.0263) (0.0266)
## Loan-to-Deposit -0.0270 -0.0103
## (0.0285) (0.0338)
## Book Equity Ratio -0.0502 -0.0517
## (0.0261) (0.0264)
## Wholesale Funding 0.0244 0.0217
## (0.0176) (0.0177)
## ROA -0.0258 -0.0215
## (0.0236) (0.0240)
## Par Benefit (z) 0.0132
## (0.0154)
## OMO Collateral (z) 0.0261
## (0.0283)
## ______________________ __________ __________
## S.E. type Hete.-rob. Hete.-rob.
## Observations 433 433
## R2 0.08055 0.08293
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
4c. Substitution
Test: BTFP-Only vs. DW-Only
# ==============================================================================
# MODEL 4c: SUBSTITUTION TEST
#
# Sample: Single-facility borrowers (BTFP-Only + DW-Only)
# DV: chose_btfp = 1 if BTFP-only, 0 if DW-only
#
# This tests: Among banks that chose only one Fed facility, what drove the
# choice of BTFP over DW?
# Predictions:
# - Banks with larger OMO MTM losses → chose BTFP (par subsidy)
# - Banks with more OMO collateral → chose BTFP
# - DW stigma effect: conditional on fundamentals, banks avoid DW
# ==============================================================================
cat("=== MODEL 4c: SUBSTITUTION (BTFP-Only vs DW-Only) ===\n\n")
## === MODEL 4c: SUBSTITUTION (BTFP-Only vs DW-Only) ===
cat("Single-facility borrowers:", nrow(df_substitution), "\n")
## Single-facility borrowers: 722
cat(" BTFP-only:", sum(df_substitution$chose_btfp), "\n")
## BTFP-only: 395
cat(" DW-only:", sum(df_substitution$chose_btfp == 0), "\n\n")
## DW-only: 327
# Base specification
mod4c_base <- safe_run_model(
df_substitution, "chose_btfp", EXPL_BASE, min_n = 20
)
# With facility characteristics
mod4c_facility <- safe_run_model(
df_substitution, "chose_btfp", EXPL_FACILITY, min_n = 20
)
# With par × uninsured interaction (stigma amplification)
EXPL_STIGMA <- paste0(EXPL_FACILITY, " + par_x_uninsured")
mod4c_stigma <- safe_run_model(
df_substitution, "chose_btfp", EXPL_STIGMA, min_n = 20
)
models_m4c <- list(
"Substitution (Base)" = mod4c_base,
"Substitution (Facility)" = mod4c_facility,
"Substitution (Stigma)" = mod4c_stigma
)
save_etable(
models = models_m4c,
filename = "M4c_btfp_vs_dw_substitution",
title_text = "Model 4c: BTFP vs. DW Substitution — Single-Facility Borrowers",
notes_text = paste0(
"LPM. Sample: banks that borrowed from exactly one Fed facility during Crisis. ",
"DV = 1 if BTFP-only, 0 if DW-only. ",
"Par Benefit = proportional MTM loss on OMO-eligible securities. ",
"OMO Collateral = OMO-eligible securities / total assets. ",
"Par $\\times$ Uninsured tests whether the par subsidy is more valuable for banks ",
"facing uninsured deposit pressure (stigma amplification). ",
"Robust SEs. z-standardized regressors."
)
)
cat("=== MODEL 4c RESULTS ===\n")
## === MODEL 4c RESULTS ===
etable(models_m4c[!sapply(models_m4c, is.null)],
fitstat = ~ n + r2, se.below = TRUE)
## Substitu.. Substitu...1 Substitu...2
## Dependent Var.: chose_btfp chose_btfp chose_btfp
##
## Constant 0.5364*** 0.5378*** 0.5341***
## (0.0243) (0.0242) (0.0245)
## MTM Loss (z) 0.0092 0.0277 0.0239
## (0.0240) (0.0243) (0.0245)
## Uninsured Leverage (z) 0.0274 0.0270 0.0257
## (0.0231) (0.0223) (0.0225)
## MTM $\times$ Uninsured -0.0193 -0.0197 -0.0135
## (0.0205) (0.0199) (0.0206)
## Log(Assets) -0.0808*** -0.0791*** -0.0739**
## (0.0233) (0.0229) (0.0236)
## Cash Ratio -0.1242*** -0.1201*** -0.1228***
## (0.0300) (0.0285) (0.0287)
## Loan-to-Deposit -0.0466 -0.0115 -0.0174
## (0.0276) (0.0301) (0.0304)
## Book Equity Ratio -0.0380 -0.0431 -0.0408
## (0.0284) (0.0287) (0.0288)
## Wholesale Funding 0.0058 0.0027 0.0028
## (0.0156) (0.0155) (0.0155)
## ROA -0.0080 -0.0037 -0.0005
## (0.0232) (0.0233) (0.0234)
## Par Benefit (z) -0.0675** -0.0505
## (0.0245) (0.0314)
## OMO Collateral (z) 0.0550* 0.0544*
## (0.0227) (0.0228)
## Par $\times$ Uninsured -0.0294
## (0.0225)
## ______________________ __________ __________ __________
## S.E. type Hete.-rob. Hete.-rob. Hete.-rob.
## Observations 722 722 722
## R2 0.06253 0.07826 0.08051
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
4d. Descriptive
Evidence: Borrower Characteristics by Facility Choice
# ==============================================================================
# MODEL 4d: DESCRIPTIVE COMPARISON OF FACILITY CHOICE GROUPS
# ==============================================================================
cat("=== MODEL 4: DESCRIPTIVE EVIDENCE ===\n\n")
## === MODEL 4: DESCRIPTIVE EVIDENCE ===
desc_vars <- c("mtm_total_raw", "mtm_btfp_raw", "mtm_other_raw",
"uninsured_lev_raw", "insured_lev_raw",
"book_equity_ratio_raw", "adjusted_equity_raw",
"dfv_raw", "adjusted_equity_dssw_raw",
"ln_assets_raw", "cash_ratio_raw",
"par_benefit_raw", "collateral_capacity_raw")
desc_labels <- c("Total MTM Loss", "OMO MTM Loss", "Non-OMO MTM Loss",
"Uninsured Lev.", "Insured Lev.",
"Book Equity", "Adj. Equity (Jiang)",
"DFV", "DSSW Adj. Equity",
"Log(Assets)", "Cash Ratio",
"Par Benefit", "OMO Collateral")
# Compare across facility choice groups
facility_means <- df_emerg_borrowers %>%
group_by(facility_choice) %>%
summarise(
N = n(),
across(all_of(desc_vars), ~round(mean(., na.rm = TRUE), 3)),
.groups = "drop"
)
cat("--- Mean Characteristics by Facility Choice ---\n")
## --- Mean Characteristics by Facility Choice ---
print(facility_means, width = Inf)
## # A tibble: 3 × 15
## facility_choice N mtm_total_raw mtm_btfp_raw mtm_other_raw
## <fct> <int> <dbl> <dbl> <dbl>
## 1 DW_Only 327 5.72 0.728 4.93
## 2 BTFP_Only 395 6.18 0.875 5.05
## 3 Both 106 5.83 0.924 4.83
## uninsured_lev_raw insured_lev_raw book_equity_ratio_raw adjusted_equity_raw
## <dbl> <dbl> <dbl> <dbl>
## 1 26.8 59.0 9.10 3.38
## 2 26.2 59.9 8.19 2.01
## 3 32.0 53.1 8.14 2.31
## dfv_raw adjusted_equity_dssw_raw ln_assets_raw cash_ratio_raw par_benefit_raw
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 71.9 75.3 13.8 6.15 0.962
## 2 73.0 75.1 13.5 4.69 0.933
## 3 66.3 68.8 14.6 4.62 0.978
## collateral_capacity_raw
## <dbl>
## 1 0.009
## 2 0.012
## 3 0.011
# T-tests: BTFP-only vs DW-only
cat("\n--- T-Tests: BTFP-Only vs DW-Only ---\n")
##
## --- T-Tests: BTFP-Only vs DW-Only ---
btfp_only <- df_emerg_borrowers %>% filter(facility_choice == "BTFP_Only")
dw_only <- df_emerg_borrowers %>% filter(facility_choice == "DW_Only")
for (i in seq_along(desc_vars)) {
v <- desc_vars[i]
tt <- tryCatch(
t.test(btfp_only[[v]], dw_only[[v]]),
error = function(e) NULL
)
if (!is.null(tt)) {
stars <- case_when(
tt$p.value < 0.01 ~ "***",
tt$p.value < 0.05 ~ "**",
tt$p.value < 0.10 ~ "*",
TRUE ~ ""
)
cat(sprintf(" %-25s BTFP=%.3f DW=%.3f diff=%.3f t=%.2f %s\n",
desc_labels[i],
mean(btfp_only[[v]], na.rm = TRUE),
mean(dw_only[[v]], na.rm = TRUE),
mean(btfp_only[[v]], na.rm = TRUE) - mean(dw_only[[v]], na.rm = TRUE),
tt$statistic, stars))
}
}
## Total MTM Loss BTFP=6.175 DW=5.718 diff=0.458 t=3.07 ***
## OMO MTM Loss BTFP=0.875 DW=0.728 diff=0.147 t=2.15 **
## Non-OMO MTM Loss BTFP=5.047 DW=4.935 diff=0.112 t=0.80
## Uninsured Lev. BTFP=26.154 DW=26.774 diff=-0.621 t=-0.71
## Insured Lev. BTFP=59.928 DW=58.986 diff=0.943 t=1.02
## Book Equity BTFP=8.188 DW=9.097 diff=-0.909 t=-3.82 ***
## Adj. Equity (Jiang) BTFP=2.012 DW=3.379 diff=-1.367 t=-4.36 ***
## DFV BTFP=73.045 DW=71.907 diff=1.138 t=1.01
## DSSW Adj. Equity BTFP=75.098 DW=75.321 diff=-0.223 t=-0.21
## Log(Assets) BTFP=13.486 DW=13.833 diff=-0.347 t=-3.22 ***
## Cash Ratio BTFP=4.686 DW=6.151 diff=-1.465 t=-3.43 ***
## Par Benefit BTFP=0.933 DW=0.962 diff=-0.029 t=-2.15 **
## OMO Collateral BTFP=0.012 DW=0.009 diff=0.002 t=3.15 ***
DSSW SOLVENCY
DIAGNOSTICS
# ==============================================================================
# COMPREHENSIVE DSSW DIAGNOSTICS
# ==============================================================================
cat("=== DSSW DEPOSIT FRANCHISE VALUE DIAGNOSTICS ===\n\n")
## === DSSW DEPOSIT FRANCHISE VALUE DIAGNOSTICS ===
# Coverage
n_total <- nrow(df_2022q4)
n_beta <- sum(!is.na(df_2022q4$beta_overall))
cat(sprintf("Beta coverage: %d / %d banks (%.1f%%)\n\n",
n_beta, n_total, 100 * n_beta / n_total))
## Beta coverage: 4226 / 4292 banks (98.5%)
# DFV distribution
cat("--- DFV Distribution (raw, % of assets) ---\n")
## --- DFV Distribution (raw, % of assets) ---
print(summary(df_2022q4$dfv_raw))
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## -8.887 67.675 77.723 74.838 85.413 107.054 66
cat("\n--- DSSW Adjusted Equity Distribution (raw, %) ---\n")
##
## --- DSSW Adjusted Equity Distribution (raw, %) ---
print(summary(df_2022q4$adjusted_equity_dssw_raw))
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.223 71.797 81.603 78.951 88.751 112.129 66
# Solvency comparison: Jiang vs DSSW
cat("\n--- Solvency Classification Cross-Tab ---\n")
##
## --- Solvency Classification Cross-Tab ---
tab_compare <- df_2022q4 %>%
filter(!is.na(dssw_insolvent)) %>%
count(mtm_insolvent, dssw_insolvent) %>%
mutate(
Jiang = ifelse(mtm_insolvent == 1, "Insolvent", "Solvent"),
DSSW = ifelse(dssw_insolvent == 1, "Insolvent", "Solvent")
)
print(tab_compare %>% select(Jiang, DSSW, n))
## # A tibble: 3 × 3
## Jiang DSSW n
## <chr> <chr> <int>
## 1 Solvent Solvent 3457
## 2 Insolvent Solvent 825
## 3 <NA> Solvent 10
# Reclassification
n_jiang_insol <- sum(df_2022q4$mtm_insolvent == 1, na.rm = TRUE)
n_dssw_insol <- sum(df_2022q4$dssw_insolvent == 1, na.rm = TRUE)
n_rescued <- sum(df_2022q4$mtm_insolvent == 1 & df_2022q4$dssw_solvent == 1, na.rm = TRUE)
cat(sprintf("\nJiang insolvent: %d | DSSW insolvent: %d | Rescued by DFV: %d\n",
n_jiang_insol, n_dssw_insol, n_rescued))
##
## Jiang insolvent: 825 | DSSW insolvent: 0 | Rescued by DFV: 813
# Beta by solvency
cat("\n--- Mean Deposit Beta by DSSW Solvency ---\n")
##
## --- Mean Deposit Beta by DSSW Solvency ---
df_2022q4 %>%
filter(!is.na(beta_overall), !is.na(dssw_insolvent)) %>%
group_by(dssw_solvent) %>%
summarise(
n = n(),
mean_beta = mean(beta_overall, na.rm = TRUE),
mean_dfv = mean(dfv_raw, na.rm = TRUE),
mean_ae_jiang = mean(adjusted_equity_raw, na.rm = TRUE),
mean_ae_dssw = mean(adjusted_equity_dssw_raw, na.rm = TRUE),
.groups = "drop"
) %>%
print()
## # A tibble: 1 × 6
## dssw_solvent n mean_beta mean_dfv mean_ae_jiang mean_ae_dssw
## <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 1 4226 0.132 74.8 4.11 79.0
VISUALIZATION
Adjusted Equity:
Jiang vs. DSSW
# ==============================================================================
# SCATTER: Jiang AE vs DSSW AE, colored by borrower status
# ==============================================================================
df_scatter <- df_crisis %>%
filter(!is.na(adjusted_equity_dssw_raw)) %>%
mutate(
Borrower = case_when(
any_fed == 1 ~ "Emergency Borrower",
fhlb_user == 1 ~ "FHLB",
TRUE ~ "Non-Borrower"
)
)
p_scatter <- ggplot(df_scatter,
aes(x = adjusted_equity_raw, y = adjusted_equity_dssw_raw,
color = Borrower, alpha = Borrower)) +
geom_point(size = 1.5) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50") +
geom_hline(yintercept = 0, linetype = "dotted", color = "red") +
geom_vline(xintercept = 0, linetype = "dotted", color = "red") +
scale_color_manual(values = c("Emergency Borrower" = "#003049",
"FHLB" = "#F77F00",
"Non-Borrower" = "grey70")) +
scale_alpha_manual(values = c("Emergency Borrower" = 0.8,
"FHLB" = 0.6,
"Non-Borrower" = 0.3)) +
labs(
title = "Jiang-Style vs. DSSW Adjusted Equity",
subtitle = "DSSW AE adds deposit franchise value; many Jiang-insolvent banks become DSSW-solvent",
x = "Jiang Adjusted Equity (Book Equity - MTM Loss, %)",
y = "DSSW Adjusted Equity (Book Equity - MTM Loss + DFV, %)"
) +
theme_paper +
annotate("text", x = -5, y = 20, label = "Rescued by\nDFV", color = "darkgreen",
fontface = "italic", size = 3.5)
p_scatter

save_figure(p_scatter, "Fig_Jiang_vs_DSSW_AE", width = 10, height = 6)
Facility Choice by
Borrower Group
# ==============================================================================
# BAR CHART: Facility choice distribution by key characteristics
# ==============================================================================
df_choice_bar <- df_emerg_borrowers %>%
group_by(facility_choice) %>%
summarise(
N = n(),
Mean_MTM = mean(mtm_total_raw, na.rm = TRUE),
Mean_ParBenefit = mean(par_benefit_raw, na.rm = TRUE),
Mean_Uninsured = mean(uninsured_lev_raw, na.rm = TRUE),
.groups = "drop"
) %>%
pivot_longer(cols = starts_with("Mean_"),
names_to = "Variable", values_to = "Value") %>%
mutate(
Variable = case_when(
Variable == "Mean_MTM" ~ "MTM Loss (%)",
Variable == "Mean_ParBenefit" ~ "Par Benefit",
Variable == "Mean_Uninsured" ~ "Uninsured Lev (%)"
)
)
p_choice <- ggplot(df_choice_bar,
aes(x = facility_choice, y = Value, fill = facility_choice)) +
geom_col(width = 0.7) +
facet_wrap(~ Variable, scales = "free_y", ncol = 3) +
scale_fill_manual(values = c("DW_Only" = "#D62828",
"BTFP_Only" = "#003049",
"Both" = "#7209B7")) +
labs(
title = "Mean Characteristics by Facility Choice (Crisis Period)",
x = NULL, y = "Mean Value", fill = "Facility"
) +
theme_paper +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_choice

save_figure(p_choice, "Fig_Facility_Choice_Characteristics", width = 9, height = 5)
Deposit Beta
Distribution by Borrower Status
# ==============================================================================
# KDE: Uninsured deposit beta by borrower status
# ==============================================================================
df_beta_kde <- df_crisis %>%
filter(!is.na(uninsured_beta_w)) %>%
mutate(
Group = case_when(
any_fed == 1 ~ "Emergency Borrower",
fhlb_user == 1 ~ "FHLB",
TRUE ~ "Non-Borrower"
),
Group = factor(Group, levels = c("Non-Borrower", "FHLB", "Emergency Borrower"))
)
if (nrow(df_beta_kde) > 50) {
p_beta <- ggplot(df_beta_kde, aes(x = uninsured_beta_w, fill = Group, color = Group)) +
geom_density(alpha = 0.4, linewidth = 0.8) +
scale_fill_manual(values = c("Non-Borrower" = "grey70",
"FHLB" = "#F77F00",
"Emergency Borrower" = "#003049")) +
scale_color_manual(values = c("Non-Borrower" = "grey70",
"FHLB" = "#F77F00",
"Emergency Borrower" = "#003049")) +
labs(
title = "Distribution of Uninsured Deposit Beta by Borrower Group",
subtitle = "Higher beta → more rate-sensitive (flightier) uninsured deposits",
x = "Uninsured Deposit Beta",
y = "Density"
) +
theme_paper
# Explicitly print the plot so it shows up in RStudio/your graphics device
print(p_beta)
save_figure(p_beta, "Fig_KDE_Uninsured_Beta", width = 8, height = 5)
} else {
cat("Insufficient observations for uninsured beta density plot.\n")
}

SUMMARY OF ALL
MODELS
cat("======================================================================\n")
## ======================================================================
cat(" FOUR-MODEL ANALYSIS — SUMMARY\n")
## FOUR-MODEL ANALYSIS — SUMMARY
cat("======================================================================\n\n")
## ======================================================================
cat("MODEL 1: Main Results (Crisis, DSSW Solvency)\n")
## MODEL 1: Main Results (Crisis, DSSW Solvency)
cat(" 8 regressions: 4 borrower types × (Solvent, Insolvent)\n")
## 8 regressions: 4 borrower types × (Solvent, Insolvent)
cat(" Solvency = DSSW AE = Book Equity - MTM + DFV ≥ 0\n")
## Solvency = DSSW AE = Book Equity - MTM + DFV ≥ 0
cat(" DFV = (1-β_i) × Deposits/TA (cross-sectional DSSW approximation)\n\n")
## DFV = (1-β_i) × Deposits/TA (cross-sectional DSSW approximation)
cat("MODEL 2: Temporal — BTFP & DW, Crisis vs. Arbitrage\n")
## MODEL 2: Temporal — BTFP & DW, Crisis vs. Arbitrage
cat(" BTFP: Crisis (Sol/Ins) + Arb (Sol/Ins) = 4 regressions\n")
## BTFP: Crisis (Sol/Ins) + Arb (Sol/Ins) = 4 regressions
cat(" DW: Crisis (Sol/Ins) + Arb (Sol/Ins) = 4 regressions\n")
## DW: Crisis (Sol/Ins) + Arb (Sol/Ins) = 4 regressions
cat(" Prediction: MTM × Uninsured strong in crisis, weak in arbitrage\n\n")
## Prediction: MTM × Uninsured strong in crisis, weak in arbitrage
cat("MODEL 3: Deposit Beta Channel\n")
## MODEL 3: Deposit Beta Channel
cat(" Triple interaction: MTM × Uninsured × Uninsured_Beta\n")
## Triple interaction: MTM × Uninsured × Uninsured_Beta
cat(" Tests whether panic is amplified for banks with flightier deposits\n")
## Tests whether panic is amplified for banks with flightier deposits
cat(" 7 regressions across 4 borrower types (nested + full)\n\n")
## 7 regressions across 4 borrower types (nested + full)
cat("MODEL 4: BTFP Complementarity with DW\n")
## MODEL 4: BTFP Complementarity with DW
cat(" 4a. Multinomial: Facility choice {BTFP_Only, DW_Only, Both}\n")
## 4a. Multinomial: Facility choice {BTFP_Only, DW_Only, Both}
cat(" 4b. Complementarity: Among DW borrowers, what drove also-BTFP?\n")
## 4b. Complementarity: Among DW borrowers, what drove also-BTFP?
cat(" 4c. Substitution: BTFP-only vs DW-only, what drove the choice?\n")
## 4c. Substitution: BTFP-only vs DW-only, what drove the choice?
cat(" 4d. Descriptive evidence on facility choice characteristics\n\n")
## 4d. Descriptive evidence on facility choice characteristics
cat("======================================================================\n")
## ======================================================================
cat("Tables saved to:", TABLE_PATH, "\n")
## Tables saved to: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/Four_Model_Analysis/tables
cat("Figures saved to:", FIG_PATH, "\n")
## Figures saved to: C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/Four_Model_Analysis/figures
cat("======================================================================\n")
## ======================================================================