rm(list = ls())
library(data.table); library(dplyr); library(tidyr); library(stringr)
library(lubridate); library(purrr); library(tibble)
library(knitr); library(kableExtra)
library(ggplot2); library(scales); library(patchwork)
library(readr)
library(sandwich); library(lmtest)

# ── Helpers ──
safe_div <- function(num, denom, default = NA_real_) {
  ifelse(is.na(denom) | denom == 0, default, num / denom)
}
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])
}
fmt  <- function(x, d = 0) formatC(x, format = "f", digits = d, big.mark = ",")
fmt2 <- function(x) fmt(x, 2)
fmt3 <- function(x) fmt(x, 3)
fmt4 <- function(x) fmt(x, 4)
z_std <- function(x) {
  mu <- mean(x, na.rm = TRUE); s <- sd(x, na.rm = TRUE)
  if (is.na(s) || s == 0) return(rep(0, length(x)))
  (x - mu) / s
}
pct <- function(x) sprintf("%.1f%%", 100 * mean(x, na.rm = TRUE))

# ── Paths ──
BASE_PATH  <- "C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/Research 2026"
DATA_PROC  <- file.path(BASE_PATH, "01_data/Final dataset")
OUT_PATH   <- file.path(BASE_PATH, "03_documentation/Corrected_Descriptive_Stats")
TABLE_PATH <- file.path(OUT_PATH, "tables")
FIG_PATH   <- file.path(OUT_PATH, "figures")
for (p in c(TABLE_PATH, FIG_PATH)) if (!dir.exists(p)) dir.create(p, recursive = TRUE)

# ── Calibration (Theory Eq. 3) ──
y_10yr      <- 0.0370        # end-2022 10-year Treasury yield
delta_decay <- 0.10          # franchise decay rate
cap_factor  <- 1 / (y_10yr + delta_decay)

# ── Period definitions ──
P0_start <- as.Date("2023-03-01"); P0_end <- as.Date("2023-03-07")
P1_start <- as.Date("2023-03-08"); P1_end <- as.Date("2023-03-12")
P2_start <- as.Date("2023-03-13"); P2_end <- as.Date("2023-04-27")
P3_start <- as.Date("2023-04-28"); P3_end <- as.Date("2023-05-04")
CRISIS_START <- P1_start; CRISIS_END <- P3_end
ARB_START <- as.Date("2023-11-15"); ARB_END <- as.Date("2024-01-24")
BTFP_LAUNCH <- as.Date("2023-03-13"); BTFP_CLOSE <- as.Date("2024-03-11")

theme_gp <- theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", size = 14),
        plot.subtitle = element_text(color = "grey40", size = 11),
        legend.position = "bottom", panel.grid.minor = element_blank())
fac_colors <- c("BTFP Only" = "#1565C0", "DW Only" = "#E53935",
                "Both" = "#7B1FA2", "Non-Borrower" = "grey60")

1 LOAD RAW DATA & SAMPLE CONSTRUCTION

# ── Call report ──
call_q <- read_csv(file.path(DATA_PROC, "final_call_gsib.csv"),
                   show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))

# ── Loan-level data ──
btfp_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),
         btfp_repayment_date = mdy(btfp_repayment_date),
         btfp_maturity_date = mdy(btfp_maturity_date))

dw_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),
         dw_repayment_date = ymd(dw_repayment_date),
         dw_credit_type_clean = str_squish(as.character(dw_credit_type))) %>%
  filter(str_detect(dw_credit_type_clean, "^Primary Credit"))


# ── DSSW betas (2022Q4 for crisis; 2023Q3 for arb) ──
dssw_betas <- read_csv(file.path(DATA_PROC, "dssw_deposit_betas.csv"),
                       show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))
dssw_beta_2022q4 <- dssw_betas %>% filter(estimation_date == "2022Q4") %>%
  select(idrssd, beta_overall, beta_insured, beta_uninsured,
         beta_insured_w, beta_uninsured_w, gamma_hat, alpha_hat)
dssw_beta_2023q3 <- dssw_betas %>% filter(estimation_date == "2023Q3") %>%
  select(idrssd, beta_overall, beta_insured, beta_uninsured,
         beta_insured_w, beta_uninsured_w, gamma_hat, alpha_hat)

# ── Deposit costs ──
deposit_costs_file <- file.path(DATA_PROC, "dssw_deposit_costs.csv")
HAS_COSTS <- file.exists(deposit_costs_file)
if (HAS_COSTS) {
  dep_costs_all <- read_csv(deposit_costs_file, show_col_types = FALSE) %>%
    mutate(idrssd = as.character(idrssd))
  deposit_costs_2022q4 <- dep_costs_all %>% filter(period == "2022Q4") %>%
    select(idrssd, deposit_cost_weighted, deposit_cost_insured, deposit_cost_uninsured)
  deposit_costs_2023q3 <- dep_costs_all %>% filter(period == "2023Q3") %>%
    select(idrssd, deposit_cost_weighted, deposit_cost_insured, deposit_cost_uninsured)
}

# ── Public bank flag (also contains BHC info: rssd_top, bhc_name) ──
public_flag_raw <- read_csv(file.path(DATA_PROC, "public_bank_flag.csv"),
                            show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))
public_flag <- public_flag_raw %>% select(idrssd, period, is_public)
# BHC lookup: a bank is in a BHC if rssd_top is non-missing
bhc_flag <- public_flag_raw %>%
  filter(!is.na(rssd_top)) %>%
  distinct(idrssd, rssd_top, bhc_name) %>%
  mutate(is_bhc = 1L)

cat(sprintf("Call report:  %s bank-quarters, %s unique banks\n",
    fmt(nrow(call_q)), fmt(n_distinct(call_q$idrssd))))
## Call report:  75,989 bank-quarters, 5,074 unique banks
cat(sprintf("DW raw:      %s loans, %s unique banks\n",
    fmt(nrow(dw_raw)), fmt(n_distinct(dw_raw$rssd_id))))
## DW raw:      9,673 loans, 1,468 unique banks
cat(sprintf("BTFP raw:    %s loans, %s unique banks\n",
    fmt(nrow(btfp_raw)), fmt(n_distinct(btfp_raw$rssd_id))))
## BTFP raw:    6,734 loans, 1,327 unique banks

1.1 Sample Construction: Step-by-Step Tracking

# ── Identify exclusions ──
call_2022q4 <- call_q %>% filter(period == "2022Q4")
call_2023q3 <- call_q %>% filter(period == "2023Q3")
failed_ids <- call_q %>% filter(period == "2022Q4", failed_bank == 1) %>% pull(idrssd) %>% unique()
gsib_ids   <- call_q %>% filter(period == "2022Q4", gsib == 1) %>% pull(idrssd) %>% unique()
excluded   <- union(failed_ids, gsib_ids)

cat("### Failed Banks:\n")

1.1.1 Failed Banks:

cat(paste(failed_ids, collapse = ", "), "\n\n")

802866, 1216826, 2942690, 3076604, 3350724, 3390627, 3437483, 4114567

cat("### G-SIB IDs:\n")

1.1.2 G-SIB IDs:

cat(paste(gsib_ids, collapse = ", "), "\n\n")

35301, 93619, 212465, 214807, 229913, 304913, 352745, 398668, 413208, 451965, 476810, 480228, 497404, 541101, 651448, 670560, 722777, 812164, 852218, 925411, 1015560, 1225761, 1443266, 1456501, 2121196, 2182786, 2325882, 2362458, 2489805, 2980209, 3212149, 3357620, 3783948

# ── Step-by-step funnel ──
step_log <- tibble(
  Step = c(
    "1. Raw call report (2022Q4)",
    "2. Exclude failed banks",
    "3. Exclude G-SIBs",
    "4. Has uninsured deposit data (D^U > 0)",
    "5. Has OMO-eligible securities (S_omo > 0)",
    "6. Merge DSSW beta (beta^U available)",
    "7. Final regression sample (all covariates non-missing)"
  ),
  N_banks = NA_integer_
)

s1 <- call_2022q4
step_log$N_banks[1] <- n_distinct(s1$idrssd)

s2 <- s1 %>% filter(!idrssd %in% failed_ids)
step_log$N_banks[2] <- n_distinct(s2$idrssd)

s3 <- s2 %>% filter(!idrssd %in% gsib_ids)
step_log$N_banks[3] <- n_distinct(s3$idrssd)

s4 <- s3 %>% filter(uninsured_deposit > 0)
step_log$N_banks[4] <- n_distinct(s4$idrssd)

s5 <- s4 %>% filter(omo_eligible > 0)
step_log$N_banks[5] <- n_distinct(s5$idrssd)

# Banks with DSSW beta
ids_with_beta <- dssw_beta_2022q4 %>% filter(!is.na(beta_uninsured)) %>% pull(idrssd)
s6 <- s4 %>% filter(idrssd %in% ids_with_beta)
step_log$N_banks[6] <- n_distinct(s6$idrssd)

# Banks missing beta
no_beta_ids <- setdiff(s4$idrssd, ids_with_beta)
cat(sprintf("\n### Banks without uninsured beta: %d\n", length(no_beta_ids)))

1.1.3 Banks without uninsured beta: 45

if (length(no_beta_ids) <= 20) cat(paste(no_beta_ids, collapse = ", "), "\n")

# Banks with missing MTM loss
no_mtm <- s3 %>% filter(is.na(mtm_loss_to_total_asset)) %>% pull(idrssd)
cat(sprintf("### Banks without MTM loss: %d\n", length(no_mtm)))

1.1.4 Banks without MTM loss: 18

kbl(step_log, format = "html", caption = "Sample Construction Funnel (2022Q4 Baseline)") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
Sample Construction Funnel (2022Q4 Baseline)
Step N_banks
  1. Raw call report (2022Q4)
4737
  1. Exclude failed banks
4729
  1. Exclude G-SIBs
4696
  1. Has uninsured deposit data (D^U > 0)
4637
  1. Has OMO-eligible securities (S_omo > 0)
4371
  1. Merge DSSW beta (beta^U available)
4592
  1. Final regression sample (all covariates non-missing)
NA

2 BUILD ANALYSIS PANELS

# ══════════════════════════════════════════════════════════════════════════
# REUSABLE FUNCTION: Build structural variables from any call report quarter
# ══════════════════════════════════════════════════════════════════════════
build_structural <- function(call_df, beta_df, cost_df = NULL, pub_df = NULL, 
                              pub_period = NULL) {
  out <- call_df %>%
    filter(!idrssd %in% excluded) %>%
    left_join(beta_df, by = "idrssd")
  
  if (!is.null(cost_df)) out <- out %>% left_join(cost_df, by = "idrssd")
  if (!is.null(pub_df) && !is.null(pub_period)) {
    out <- out %>%
      left_join(pub_df %>% filter(period == pub_period) %>% select(idrssd, is_public),
                by = "idrssd") %>%
      mutate(is_public = replace_na(is_public, 0L))
  }
  
  out %>% mutate(
    # ── Deposit betas & franchise ──
    beta_u_clipped = pmin(pmax(ifelse(!is.na(beta_uninsured), beta_uninsured, NA_real_), 0), 1),
    cost_u = if ("deposit_cost_uninsured" %in% names(.)) {
      ifelse(!is.na(deposit_cost_uninsured), deposit_cost_uninsured, 0)
    } else 0,
    du_ta = uninsured_deposit_to_total_asset,
    d_ta  = total_deposit_to_total_asset,
    du_d  = uninsured_to_deposit,
    di_d  = safe_div(insured_deposit, total_deposit, NA_real_),
    
    # Franchise: Theory Eq. 3, F = (1-β^U)*r*D^U / δ, calibrated via DSSW
    gross_rent = (1 - beta_u_clipped) * y_10yr,
    net_rent   = gross_rent - cost_u,
    f_pp  = ifelse(!is.na(beta_u_clipped), pmax(net_rent * cap_factor * du_ta, 0) * 100, NA_real_),
    fu_pp = ifelse(!is.na(f_pp), du_d * f_pp, NA_real_),
    fi_pp = ifelse(!is.na(f_pp), di_d * f_pp, NA_real_),
    
    # ── MTM losses (pp of TA) ──
    ell        = mtm_loss_to_total_asset,
    ell_sec    = loss_total_sec_to_total_asset,
    ell_omo    = loss_omo_to_total_asset,
    ell_loan   = loss_total_loan_to_total_asset,
    ell_nonomo = loss_nonomo_sec_to_total_asset,
    
    # ── Balance sheet ratios (pp) ──
    eq_ta    = book_equity_to_total_asset,
    cash_ta  = cash_to_total_asset,
    sec_ta   = security_to_total_asset,
    omo_ta   = omo_eligible_to_total_asset,
    dep_ta   = total_deposit_to_total_asset,
    uid_ta   = uninsured_deposit_to_total_asset,
    log_ta   = log(total_asset),
    loan_ta  = total_loan_to_total_asset,
    fhlb_ta  = fhlb_to_total_asset,
    one_minus_beta = 1 - beta_u_clipped,
    
    # ── Market-value equity E^MV (Eq. 4): E^MV/A = E/A - λ/A + F/A ──
    emv_pp = ifelse(!is.na(f_pp), eq_ta - ell + f_pp, NA_real_),
    
    # ── Run value v (Eq. 5): v/A = E^MV/A - F^U/A ──
    v_pp = ifelse(!is.na(emv_pp) & !is.na(fu_pp), emv_pp - fu_pp, NA_real_),
    v_no_franchise = eq_ta - ell,
    
    # ── Flags ──
    run_possible         = as.integer(!is.na(v_pp) & v_pp < 0),
    fundamental_insolvent = as.integer(!is.na(emv_pp) & emv_pp < 0),
    book_insolvent       = as.integer(v_no_franchise < 0),
    
    # ── OMO securities ──
    omo_book = omo_eligible,
    omo_loss = loss_omo_total,
    omo_mv   = pmax(omo_book - omo_loss, 0),
    
    # ── Liquidity components ──
    C   = replace_na(cash, 0),
    D_U = replace_na(uninsured_deposit, 0),
    D   = replace_na(insured_deposit, 0) + D_U,
    avail_liq_omo = C + omo_mv,
    
    # ── Coverage ratios (Theory Table 1) ──
    phi   = safe_div(avail_liq_omo, D_U, NA_real_),
    phi_C = safe_div(C, D_U, NA_real_),
    phi_S = safe_div(omo_mv, D_U, NA_real_),
    rho_0 = safe_div(C + replace_na(omo_eligible, 0), D_U, NA_real_),
    
    # ── Liquidity gap ──
    liq_gap = D_U - avail_liq_omo,
    has_gap = as.integer(liq_gap > 0),
    
    # ── BTFP subsidy ──
    btfp_subsidy    = pmax(loss_omo_total, 0),
    btfp_subsidy_ta = 100 * btfp_subsidy / total_asset,
    
    # ── Thresholds ──
    lam_lower = eq_ta + fi_pp,
    lam_upper = eq_ta + f_pp,
    
    # ── Wholesale funding ──
    wholesale_ta = 100 * (fhlb_adv + fed_fund_purchase + repo) / total_asset,
    
    # ── Theory region classification ──
    theory_region = factor(case_when(
      is.na(v_pp) | is.na(emv_pp) ~ NA_character_,
      v_pp >= 0                    ~ "Safe",
      v_pp < 0 & emv_pp > 0       ~ "Panic Zone",
      emv_pp <= 0                  ~ "Insolvency"
    ), levels = c("Safe", "Panic Zone", "Insolvency")),
    
    # ── Alternative franchise calibrations for robustness ──
    # (A) No franchise
    emv_A = eq_ta - ell,
    v_A   = eq_ta - ell,
    # (B) Full deposit franchise on D (DSSW style)
    f_B   = ifelse(!is.na(beta_u_clipped), pmax(net_rent * cap_factor * d_ta, 0) * 100, NA_real_),
    fu_B  = ifelse(!is.na(f_B), du_d * f_B, NA_real_),
    fi_B  = ifelse(!is.na(f_B), di_d * f_B, NA_real_),
    emv_B = ifelse(!is.na(f_B), eq_ta - ell + f_B, NA_real_),
    v_B   = ifelse(!is.na(emv_B) & !is.na(fu_B), emv_B - fu_B, NA_real_),
    region_B = factor(case_when(
      is.na(v_B) | is.na(emv_B) ~ NA_character_,
      v_B >= 0                   ~ "Safe",
      v_B < 0 & emv_B > 0       ~ "Panic Zone",
      emv_B <= 0                 ~ "Insolvency"
    ), levels = c("Safe", "Panic Zone", "Insolvency")),
    # (C) = main spec (already computed as f_pp, fu_pp, etc.)
    f_C = f_pp, fu_C = fu_pp, fi_C = fi_pp,
    emv_C = emv_pp, v_C = v_pp, region_C = theory_region,
    
    # ── Size bins ──
    size_bin = factor(case_when(
      total_asset >= 10e6  ~ "Large (>$10B)",
      total_asset >= 1e6   ~ "Medium ($1-10B)",
      TRUE                 ~ "Small (<$1B)"
    ), levels = c("Small (<$1B)", "Medium ($1-10B)", "Large (>$10B)"))
  )
}

# ── Apply exclusions to loan data ──
dw_loans   <- dw_raw   %>% filter(!rssd_id %in% excluded)
btfp_loans <- btfp_raw %>% filter(!rssd_id %in% excluded)

# ── Aggregate borrowing to bank level (all periods) ──
dw_bank_all <- dw_loans %>%
  group_by(rssd_id) %>%
  summarise(dw_total = sum(dw_loan_amount), dw_n = n(),
    dw_first = min(dw_loan_date), dw_last = max(dw_loan_date),
    dw_mean_rate = mean(dw_interest_rate, na.rm=T),
    dw_mean_term = mean(dw_term, na.rm=T),
    dw_mean_effmat = mean(dw_effective_maturity_days, na.rm=T),
    .groups = "drop") %>% rename(idrssd = rssd_id)

btfp_bank_all <- btfp_loans %>%
  group_by(rssd_id) %>%
  summarise(btfp_total = sum(btfp_loan_amount), btfp_n = n(),
    btfp_first = min(btfp_loan_date), btfp_last = max(btfp_loan_date),
    btfp_mean_rate = mean(btfp_interest_rate, na.rm=T),
    btfp_mean_term = mean(btfp_term, na.rm=T),
    btfp_mean_effmat = mean(btfp_effective_maturity_days, na.rm=T),
    .groups = "drop") %>% rename(idrssd = rssd_id)

# ══════════════════════════════════════════════════════════════════════════
# BUILD CRISIS PANEL (2022Q4 baseline)
# ══════════════════════════════════════════════════════════════════════════
df <- build_structural(call_2022q4, dssw_beta_2022q4,
        if (HAS_COSTS) deposit_costs_2022q4 else NULL,
        public_flag, "2022Q4") %>%
  left_join(dw_bank_all, by = "idrssd") %>%
  left_join(btfp_bank_all, by = "idrssd") %>%
  mutate(
    dw_total = replace_na(dw_total, 0), btfp_total = replace_na(btfp_total, 0),
    dw_n = replace_na(dw_n, 0L), btfp_n = replace_na(btfp_n, 0L),
    g = dw_total + btfp_total, g_000 = g / 1000,
    used_dw   = as.integer(dw_total > 0),
    used_btfp = as.integer(btfp_total > 0),
    borrower_type = factor(case_when(
      used_dw == 1 & used_btfp == 1 ~ "Both",
      used_dw == 1 ~ "DW Only",
      used_btfp == 1 ~ "BTFP Only",
      TRUE ~ "Non-Borrower"),
      levels = c("Non-Borrower", "DW Only", "BTFP Only", "Both")),
    borrowed = as.integer(g > 0),
    g_du = safe_div(g_000, D_U, NA_real_)
  ) %>%
  left_join(bhc_flag %>% select(idrssd, is_bhc, bhc_name), by = "idrssd") %>%
  mutate(is_bhc = replace_na(is_bhc, 0L))

# ── Regression sample: record step 7 ──
df_reg_check <- df %>%
  filter(!is.na(ell), !is.na(du_d), !is.na(phi), !is.na(ell_omo),
         !is.na(eq_ta), !is.na(cash_ta), !is.na(log_ta), !is.na(roa),
         !is.na(loan_to_deposit), !is.na(wholesale_ta))
step_log$N_banks[7] <- n_distinct(df_reg_check$idrssd)

cat(sprintf("Crisis panel: %s banks (2022Q4 baseline)\n", fmt(nrow(df))))
## Crisis panel: 4,700 banks (2022Q4 baseline)
print(table(df$borrower_type))
## 
## Non-Borrower      DW Only    BTFP Only         Both 
##         2531          862          730          577
cat(sprintf("\nSize distribution:\n")); print(table(df$size_bin))
## 
## Size distribution:
## 
##    Small (<$1B) Medium ($1-10B)   Large (>$10B) 
##            3743             819             138
# ══════════════════════════════════════════════════════════════════════════
# BUILD ARBITRAGE PANEL (2023Q3 baseline) — only arb-window loans
# ══════════════════════════════════════════════════════════════════════════
arb_dw_bank <- dw_loans %>%
  filter(dw_loan_date >= ARB_START, dw_loan_date <= ARB_END) %>%
  group_by(rssd_id) %>%
  summarise(dw_total = sum(dw_loan_amount), dw_n = n(), .groups = "drop") %>%
  rename(idrssd = rssd_id)

arb_btfp_bank <- btfp_loans %>%
  filter(btfp_loan_date >= ARB_START, btfp_loan_date <= ARB_END) %>%
  group_by(rssd_id) %>%
  summarise(btfp_total = sum(btfp_loan_amount), btfp_n = n(), .groups = "drop") %>%
  rename(idrssd = rssd_id)

df_arb <- build_structural(call_2023q3, dssw_beta_2023q3,
            if (HAS_COSTS) deposit_costs_2023q3 else NULL,
            public_flag, "2023Q3") %>%
  left_join(arb_dw_bank, by = "idrssd") %>%
  left_join(arb_btfp_bank, by = "idrssd") %>%
  mutate(
    dw_total = replace_na(dw_total, 0), btfp_total = replace_na(btfp_total, 0),
    dw_n = replace_na(dw_n, 0L), btfp_n = replace_na(btfp_n, 0L),
    g = dw_total + btfp_total, g_000 = g / 1000,
    used_dw   = as.integer(dw_total > 0),
    used_btfp = as.integer(btfp_total > 0),
    borrower_type = factor(case_when(
      used_dw == 1 & used_btfp == 1 ~ "Both",
      used_dw == 1 ~ "DW Only",
      used_btfp == 1 ~ "BTFP Only",
      TRUE ~ "Non-Borrower"),
      levels = c("Non-Borrower", "DW Only", "BTFP Only", "Both")),
    borrowed = as.integer(g > 0),
    g_du = safe_div(g_000, D_U, NA_real_),
    size_bin = factor(case_when(
      total_asset >= 10e6  ~ "Large (>$10B)",
      total_asset >= 1e6   ~ "Medium ($1-10B)",
      TRUE                 ~ "Small (<$1B)"
    ), levels = c("Small (<$1B)", "Medium ($1-10B)", "Large (>$10B)"))
  )

cat(sprintf("Arbitrage panel: %s banks (2023Q3 baseline)\n", fmt(nrow(df_arb))))
## Arbitrage panel: 4,604 banks (2023Q3 baseline)
print(table(df_arb$borrower_type))
## 
## Non-Borrower      DW Only    BTFP Only         Both 
##         3581          257          655          111
# ── Crisis / Arb period tags for crisis panel ──
crisis_dw   <- dw_loans %>% filter(dw_loan_date >= CRISIS_START, dw_loan_date <= CRISIS_END) %>% pull(rssd_id) %>% unique()
crisis_btfp <- btfp_loans %>% filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= CRISIS_END) %>% pull(rssd_id) %>% unique()
arb_dw_ids  <- dw_loans %>% filter(dw_loan_date >= ARB_START, dw_loan_date <= ARB_END) %>% pull(rssd_id) %>% unique()
arb_btfp_ids<- btfp_loans %>% filter(btfp_loan_date >= ARB_START, btfp_loan_date <= ARB_END) %>% pull(rssd_id) %>% unique()

df <- df %>% mutate(
  in_crisis = as.integer(idrssd %in% union(crisis_dw, crisis_btfp)),
  in_arb    = as.integer(idrssd %in% union(arb_dw_ids, arb_btfp_ids))
)

3 TABLE 1: LOAN DETAIL BY PERIOD

Period-wise loan characteristics for DW and BTFP, including rates, terms, collateral, and advance rates.

period_defs <- tribble(
  ~label,                        ~start,       ~end,
  "Pre-Crisis (Mar 1-7)",        P0_start,     P0_end,
  "SVB Week (Mar 8-12)",         P1_start,     P1_end,
  "BTFP Active (Mar 13-Apr 27)", P2_start,     P2_end,
  "FRC Week (Apr 28-May 4)",     P3_start,     P3_end,
  "Crisis Total (Mar 8-May 4)",  CRISIS_START, CRISIS_END,
  "Arbitrage (Nov 15-Jan 24)",   ARB_START,    ARB_END,
  "Entire BTFP (Mar 13-Mar 11)", BTFP_LAUNCH,  BTFP_CLOSE
)

# ── DW loan detail ──
dw_loan_detail <- map_dfr(1:nrow(period_defs), function(i) {
  sub <- dw_loans %>% filter(dw_loan_date >= period_defs$start[i],
                             dw_loan_date <= period_defs$end[i])
  if (nrow(sub) == 0) return(tibble(Period = period_defs$label[i], Banks = 0, Loans = 0,
    `Total ($B)` = 0, `Mean ($M)` = NA, `Median ($M)` = NA,
    `Rate (%)` = NA, `Term (d)` = NA, `Eff Mat (d)` = NA, `Adv Rate (%)` = NA))
  tibble(
    Period       = period_defs$label[i],
    Banks        = n_distinct(sub$rssd_id),
    Loans        = nrow(sub),
    `Total ($B)` = round(sum(sub$dw_loan_amount, na.rm=T) / 1e9, 2),
    `Mean ($M)`  = round(mean(sub$dw_loan_amount, na.rm=T) / 1e6, 2),
    `Median ($M)`= round(median(sub$dw_loan_amount, na.rm=T) / 1e6, 2),
    `Rate (%)`   = round(mean(sub$dw_interest_rate, na.rm=T), 3),
    `Term (d)`   = round(mean(sub$dw_term, na.rm=T), 1),
    `Eff Mat (d)`= round(mean(sub$dw_effective_maturity_days, na.rm=T), 1),
    `Loan/Coll (%)`= round(100 * mean(safe_div(sub$dw_loan_amount, sub$dw_total_collateral, NA), na.rm=T), 1)
  )
})

cat("## Panel A: Discount Window\n\n")

3.1 Panel A: Discount Window

kbl(dw_loan_detail, format = "html", caption = "DW Loan Characteristics by Sub-Period") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  footnote(general = "DW lends at market value against any pre-pledged collateral (Eq. 15).")
DW Loan Characteristics by Sub-Period
Period Banks Loans Total (\(B) </th> <th style="text-align:right;"> Mean (\)M) Median ($M) Rate (%) Term (d) Eff Mat (d) Loan/Coll (%)
Pre-Crisis (Mar 1-7) 72 171 9.46 55.29 10.00 4.750 5.3 4.1 24.2
SVB Week (Mar 8-12) 68 124 10.10 81.47 10.47 4.750 3.8 3.0 22.2
BTFP Active (Mar 13-Apr 27) 402 1488 518.46 348.43 10.00 4.921 4.4 3.0 27.5
FRC Week (Apr 28-May 4) 101 213 9.61 45.13 8.00 5.050 3.8 2.8 24.0
Crisis Total (Mar 8-May 4) 453 1825 538.18 294.89 9.70 4.924 4.3 3.0 26.8
Arbitrage (Nov 15-Jan 24) 374 985 14.84 15.06 3.90 5.500 4.9 3.5 17.3
Entire BTFP (Mar 13-Mar 11) 1358 7735 668.51 86.43 6.31 5.306 4.6 3.4 22.6
Note:
DW lends at market value against any pre-pledged collateral (Eq. 15).
# ── BTFP loan detail ──
btfp_loan_detail <- map_dfr(1:nrow(period_defs), function(i) {
  sub <- btfp_loans %>% filter(btfp_loan_date >= period_defs$start[i],
                               btfp_loan_date <= period_defs$end[i])
  if (nrow(sub) == 0) return(tibble(Period = period_defs$label[i], Banks = 0, Loans = 0,
    `Total ($B)` = 0, `Mean ($M)` = NA, `Median ($M)` = NA,
    `Rate (%)` = NA, `Term (d)` = NA, `Eff Mat (d)` = NA, `Loan/Coll (%)` = NA))
  tibble(
    Period       = period_defs$label[i],
    Banks        = n_distinct(sub$rssd_id),
    Loans        = nrow(sub),
    `Total ($B)` = round(sum(sub$btfp_loan_amount, na.rm=T) / 1e9, 2),
    `Mean ($M)`  = round(mean(sub$btfp_loan_amount, na.rm=T) / 1e6, 2),
    `Median ($M)`= round(median(sub$btfp_loan_amount, na.rm=T) / 1e6, 2),
    `Rate (%)`   = round(mean(sub$btfp_interest_rate, na.rm=T), 3),
    `Term (d)`   = round(mean(sub$btfp_term, na.rm=T), 1),
    `Eff Mat (d)`= round(mean(sub$btfp_effective_maturity_days, na.rm=T), 1),
    `Loan/Coll (%)`= round(100 * mean(safe_div(sub$btfp_loan_amount, sub$btfp_total_collateral, NA), na.rm=T), 1)
  )
})

cat("\n## Panel B: Bank Term Funding Program\n\n")

3.2 Panel B: Bank Term Funding Program

kbl(btfp_loan_detail, format = "html", caption = "BTFP Loan Characteristics by Sub-Period") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  footnote(general = "BTFP lends at par against OMO-eligible securities only (Eq. 16). Launched Mar 13, 2023.")
BTFP Loan Characteristics by Sub-Period
Period Banks Loans Total (\(B) </th> <th style="text-align:right;"> Mean (\)M) Median ($M) Rate (%) Term (d) Eff Mat (d) Loan/Coll (%)
Pre-Crisis (Mar 1-7) 0 0 0.00 NA NA NA NA NA NA
SVB Week (Mar 8-12) 0 0 0.00 NA NA NA NA NA NA
BTFP Active (Mar 13-Apr 27) 467 1025 120.16 117.23 15 4.678 315.0 152.1 53.1
FRC Week (Apr 28-May 4) 183 278 11.32 40.74 10 4.787 322.2 177.9 45.3
Crisis Total (Mar 8-May 4) 526 1303 131.49 100.91 13 4.701 316.5 157.6 51.4
Arbitrage (Nov 15-Jan 24) 780 3098 219.45 70.84 15 4.948 338.0 88.1 50.5
Entire BTFP (Mar 13-Mar 11) 1316 6695 410.36 61.29 10 5.019 311.1 104.5 45.1
Note:
BTFP lends at par against OMO-eligible securities only (Eq. 16). Launched Mar 13, 2023.

4 TABLE 2: REPEAT vs NEW BORROWERS

# ── Pre-crisis DW borrowers ──
pre_crisis_dw <- dw_loans %>% filter(dw_loan_date < CRISIS_START) %>%
  pull(rssd_id) %>% unique()

# ── Phase classification for each bank ──
dw_by_phase <- dw_loans %>%
  mutate(phase = case_when(
    dw_loan_date < CRISIS_START ~ "Pre-Crisis",
    dw_loan_date >= P1_start & dw_loan_date <= P1_end ~ "P1_SVB",
    dw_loan_date >= P2_start & dw_loan_date <= P2_end ~ "P2_BTFP",
    dw_loan_date >= P3_start & dw_loan_date <= P3_end ~ "P3_FRC",
    dw_loan_date >= ARB_START & dw_loan_date <= ARB_END ~ "Arbitrage",
    TRUE ~ "Other")) %>%
  filter(phase != "Other") %>%
  distinct(rssd_id, phase) %>% rename(idrssd = rssd_id)

btfp_by_phase <- btfp_loans %>%
  mutate(phase = case_when(
    btfp_loan_date >= P2_start & btfp_loan_date <= P2_end ~ "P2_BTFP",
    btfp_loan_date >= P3_start & btfp_loan_date <= P3_end ~ "P3_FRC",
    btfp_loan_date >= ARB_START & btfp_loan_date <= ARB_END ~ "Arbitrage",
    TRUE ~ "Other")) %>%
  filter(phase != "Other") %>%
  distinct(rssd_id, phase) %>% rename(idrssd = rssd_id)

all_phases <- bind_rows(dw_by_phase, btfp_by_phase) %>% distinct(idrssd, phase)

# ── Borrower flow: cumulative tracking ──
cumulative <- pre_crisis_dw
flow_rows <- list()
phase_order <- c("Pre-Crisis", "P1_SVB", "P2_BTFP", "P3_FRC", "Arbitrage")
phase_labels <- c("Pre-Crisis (< Mar 8)", "P1: SVB Week", "P2: BTFP Active",
                   "P3: FRC Week", "Arbitrage")

for (i in seq_along(phase_order)) {
  ids <- all_phases %>% filter(phase == phase_order[i]) %>% pull(idrssd)
  returning <- intersect(ids, cumulative)
  new_ids   <- setdiff(ids, cumulative)
  cumulative <- union(cumulative, ids)
  flow_rows[[i]] <- tibble(
    Phase = phase_labels[i],
    `Total Banks` = length(ids),
    Returning = if (i == 1) NA_integer_ else length(returning),
    `New (First Time)` = length(new_ids),
    Cumulative = length(cumulative)
  )
}
flow_tbl <- bind_rows(flow_rows)

cat("## Panel A: Borrower Flow — Returning vs New Entrants\n\n")

4.1 Panel A: Borrower Flow — Returning vs New Entrants

kbl(flow_tbl, format = "html", caption = "Borrower Flow by Phase") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE) %>%
  footnote(general = "Returning = borrowed in any prior phase. New = first-time borrower.
           Cumulative = total unique banks ever borrowed up to this phase.")
Borrower Flow by Phase
Phase Total Banks Returning New (First Time) Cumulative
Pre-Crisis (< Mar 8) 264 NA 0 264
P1: SVB Week 68 46 22 286
P2: BTFP Active 774 144 630 916
P3: FRC Week 270 191 79 995
Arbitrage 1039 466 573 1568
Note:
Returning = borrowed in any prior phase. New = first-time borrower.
Cumulative = total unique banks ever borrowed up to this phase.
# ── Repeat borrowers: banks with >1 loan in same facility during crisis ──
crisis_all <- union(crisis_dw, crisis_btfp)
crisis_returning <- intersect(crisis_all, pre_crisis_dw)
crisis_new <- setdiff(crisis_all, pre_crisis_dw)

dw_repeat_crisis <- dw_loans %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= CRISIS_END) %>%
  group_by(rssd_id) %>% summarise(n = n(), .groups = "drop") %>%
  filter(n > 1) %>% pull(rssd_id)

btfp_repeat_crisis <- btfp_loans %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= CRISIS_END) %>%
  group_by(rssd_id) %>% summarise(n = n(), .groups = "drop") %>%
  filter(n > 1) %>% pull(rssd_id)

# ── Panel B: Structural comparison ──
repeat_groups <- list(
  "Returning (pre-crisis)" = df %>% filter(idrssd %in% crisis_returning),
  "New (first in crisis)"  = df %>% filter(idrssd %in% crisis_new),
  "DW Repeat (>1 DW)"     = df %>% filter(idrssd %in% dw_repeat_crisis),
  "BTFP Repeat (>1 BTFP)" = df %>% filter(idrssd %in% btfp_repeat_crisis)
)

key_vars <- c("ell", "ell_omo", "du_d", "beta_u_clipped", "f_pp", "fu_pp",
              "emv_pp", "v_pp", "phi", "eq_ta", "cash_ta", "log_ta")
key_labels <- c("MTM Loss/TA", "OMO Loss/TA", "D^U/D", "Beta^U", "F/A", "F^U/A",
                "E^MV/A", "v (run value)", "phi", "Equity/TA", "Cash/TA", "log(TA)")

repeat_summary <- imap_dfr(repeat_groups, function(d, nm) {
  map_dfr(seq_along(key_vars), function(j) {
    x <- d[[key_vars[j]]]
    tibble(Group = nm, Variable = key_labels[j], N = sum(!is.na(x)),
           Mean = round(mean(x, na.rm=T), 3), Median = round(median(x, na.rm=T), 3))
  })
}) %>%
  pivot_wider(names_from = Group, values_from = c(N, Mean, Median),
              names_glue = "{Group}_{.value}")

cat("\n## Panel B: Structural Variables — Returning vs New\n\n")

4.2 Panel B: Structural Variables — Returning vs New

kbl(repeat_summary, format = "html", caption = "Repeat vs New Borrowers: Key Variables") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 9) %>%
  footnote(general = "Returning = used DW before March 8. New = first-time during crisis.")
Repeat vs New Borrowers: Key Variables
Variable Returning (pre-crisis)_N New (first in crisis)_N DW Repeat (>1 DW)_N BTFP Repeat (>1 BTFP)_N Returning (pre-crisis)_Mean New (first in crisis)_Mean DW Repeat (>1 DW)_Mean BTFP Repeat (>1 BTFP)_Mean Returning (pre-crisis)_Median New (first in crisis)_Median DW Repeat (>1 DW)_Median BTFP Repeat (>1 BTFP)_Median
MTM Loss/TA 134 729 127 289 9.337 9.265 9.237 9.777 9.285 8.968 9.315 9.365
OMO Loss/TA 134 729 127 289 1.714 1.570 1.587 1.713 1.231 1.016 1.033 1.214
D^U/D 134 729 127 289 32.854 31.645 35.908 31.729 31.732 29.314 33.947 29.944
Beta^U 131 726 125 287 0.349 0.342 0.358 0.344 0.319 0.321 0.318 0.323
F/A 131 726 125 287 273.300 270.509 289.433 273.052 235.630 250.315 262.477 249.360
F^U/A 131 726 125 287 10554.594 10215.593 12273.614 10526.533 7598.580 7278.887 8230.241 7278.644
E^MV/A 131 726 125 287 272.601 269.918 288.649 271.605 235.222 249.879 266.628 247.290
v (run value) 131 726 125 287 -10281.993 -9945.676 -11984.965 -10254.928 -7342.130 -7039.620 -7986.002 -7038.074
phi 134 728 127 289 0.722 0.975 0.622 0.807 0.488 0.580 0.376 0.585
Equity/TA 134 729 127 289 8.379 8.636 8.251 8.320 8.052 8.520 8.266 8.369
Cash/TA 134 729 127 289 3.664 5.705 4.336 4.370 2.299 3.598 2.599 2.807
log(TA) 134 729 127 289 13.961 13.706 14.092 13.773 13.802 13.416 13.940 13.518
Note:
Returning = used DW before March 8. New = first-time during crisis.

5 ══════════════════════════════════════════════════════════════════════════

6 TABLE 3: LIQUIDITY GAP ANALYSIS

7 ══════════════════════════════════════════════════════════════════════════

# ══════════════════════════════════════════════════════════════════════════
# REUSABLE: Compute liquidity gap statistics for a group of banks
# ══════════════════════════════════════════════════════════════════════════
gap_stats <- function(data, label = "") {
  if (nrow(data) == 0) {
    return(tibble(Group = label, N = 0,
      `Liq Gap ($000)` = NA, `Cash/D^U` = NA, `S^OMO_MV/D^U` = NA,
      `rho` = NA, `rho_0` = NA, `phi_decline` = NA,
      `E^MV/A` = NA, `F/A` = NA, `F^U/A` = NA, `v` = NA,
      `Borrow ($000)` = NA, `Borrow Std ($000)` = NA, `% Gap` = NA))
  }
  tibble(
    Group = label,
    N = nrow(data),
    `Liq Gap ($000)` = round(mean(data$liq_gap, na.rm=T), 0),
    `Cash/D^U` = round(mean(data$phi_C, na.rm=T), 3),
    `S^OMO_MV/D^U` = round(mean(data$phi_S, na.rm=T), 3),
    `rho` = round(mean(data$phi, na.rm=T), 3),
    `rho_0` = round(mean(data$rho_0, na.rm=T), 3),
    `phi_decline` = round(mean(data$rho_0 - data$phi, na.rm=T), 3),
    `E^MV/A` = round(mean(data$emv_pp, na.rm=T), 2),
    `F/A` = round(mean(data$f_pp, na.rm=T), 2),
    `F^U/A` = round(mean(data$fu_pp, na.rm=T), 2),
    `v` = round(mean(data$v_pp, na.rm=T), 2),
    `Borrow ($000)` = round(mean(data$g, na.rm=T), 0),
    `Borrow Std ($000)` = if (sum(data$g > 0, na.rm=T) > 1)
      round(sd(data$g[data$g > 0], na.rm=T), 0) else NA,
    `% Gap` = sprintf("%.1f%%", 100 * mean(data$has_gap, na.rm=T))
  )
}

# ══════════════════════════════════════════════════════════════════════════
# PANEL A: Full sample — Borrower vs Non-Borrower
# ══════════════════════════════════════════════════════════════════════════
cat("## Panel A: Full Sample (Borrower vs Non-Borrower)\n\n")

7.1 Panel A: Full Sample (Borrower vs Non-Borrower)

panA <- bind_rows(
  gap_stats(df, "All Banks"),
  gap_stats(df %>% filter(borrower_type == "Non-Borrower"), "Non-Borrower"),
  gap_stats(df %>% filter(borrowed == 1), "All Borrowers"),
  gap_stats(df %>% filter(borrower_type == "DW Only"), "DW Only"),
  gap_stats(df %>% filter(borrower_type == "BTFP Only"), "BTFP Only"),
  gap_stats(df %>% filter(borrower_type == "Both"), "Both")
)

kbl(panA %>% select(-Group) %>% t() %>% as.data.frame() %>%
      setNames(panA$Group) %>% tibble::rownames_to_column("Metric"),
    format = "html", caption = "Panel A: Liquidity Gap — Full Sample by Borrower Type") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 9)
Panel A: Liquidity Gap — Full Sample by Borrower Type
Metric All Banks Non-Borrower All Borrowers DW Only BTFP Only Both
N 4700 2531 2169 862 730 577
Liq Gap ($000) 303145 60296 584509 390798 74118 1519630
Cash/D^U 1.680 1.384 2.019 4.669 0.326 0.209
SOMO_MV/DU 0.857 0.961 0.739 0.941 0.695 0.495
rho 2.536 2.340 2.758 5.610 1.020 0.704
rho_0 2.642 2.442 2.870 5.774 1.103 0.778
phi_decline 0.106 0.100 0.113 0.163 0.083 0.074
E^MV/A 241.57 226.28 258.98 259.32 246.83 273.86
F/A 240.12 223.54 258.99 257.93 247.74 274.83
F^U/A 8334.71 7405.76 9392.18 9691.48 8112.14 10562.19
v -8093.14 -7179.48 -9133.20 -9432.16 -7865.30 -10288.33
Borrow ($000) 279879527 0 606470161 133129046 233603638 1785348156
Borrow Std ($000) 7721584965 NA 7721584965 1010690056 2080555921 14680080360
% Gap 64.7% 55.6% 75.2% 74.4% 71.0% 82.0%
# ══════════════════════════════════════════════════════════════════════════
# PANEL B: By borrower type, crisis vs arb period (repeat/new)
# ══════════════════════════════════════════════════════════════════════════
#
# DEFINITION OF REPEAT vs NEW:
#   "Repeat" = bank that borrowed from DW or BTFP at ANY point during
#              the CRISIS window (Mar 1 – May 4, 2023) or pre-crisis,
#              AND ALSO borrowed during the ARBITRAGE window (Nov 15 – Jan 24).
#   "New"    = bank that borrowed ONLY during the arbitrage window,
#              having never used DW/BTFP during the crisis or pre-crisis.
#
# We identify crisis borrowers directly from loan-level data (not from
# the phase-classification approach which can miss mid-period borrowers).
# ══════════════════════════════════════════════════════════════════════════
cat("\n## Panel B: By Borrower Type — Crisis vs Arbitrage Period\n\n")

7.2 Panel B: By Borrower Type — Crisis vs Arbitrage Period

# Step 1: All banks that ever borrowed DW or BTFP up through May 4 (crisis end)
crisis_dw_all <- dw_loans %>%
  filter(dw_loan_date <= CRISIS_END) %>%
  pull(rssd_id) %>% unique()
crisis_btfp_all <- btfp_loans %>%
  filter(btfp_loan_date <= CRISIS_END) %>%
  pull(rssd_id) %>% unique()
ever_crisis_borrower <- union(crisis_dw_all, crisis_btfp_all)

# Step 2: Arb-window borrowers (from loan-level data)
arb_borrower_ids <- union(arb_dw_ids, arb_btfp_ids)

# Step 3: Classify arb borrowers as repeat or new
arb_repeat_ids <- intersect(arb_borrower_ids, ever_crisis_borrower)
arb_new_ids    <- setdiff(arb_borrower_ids, ever_crisis_borrower)

cat(sprintf("**Repeat/New classification for Arbitrage borrowers:**\n\n"))

Repeat/New classification for Arbitrage borrowers:

cat(sprintf("- Total arb borrowers (loan-level): %d\n", length(arb_borrower_ids)))
  • Total arb borrowers (loan-level): 1039
cat(sprintf("- Ever borrowed during crisis (<=May 4): %d\n", length(ever_crisis_borrower)))
  • Ever borrowed during crisis (<=May 4): 995
cat(sprintf("- Arb Repeat (also crisis borrower): %d\n", length(arb_repeat_ids)))
  • Arb Repeat (also crisis borrower): 466
cat(sprintf("- Arb New (first-time in arb window): %d\n\n", length(arb_new_ids)))
  • Arb New (first-time in arb window): 573
panB_items <- list(
  "All (Crisis)"    = df %>% filter(in_crisis == 1, borrowed == 1),
  "Repeat (Crisis)" = df %>% filter(idrssd %in% crisis_returning),
  "New (Crisis)"    = df %>% filter(idrssd %in% crisis_new),
  "All (Arb)"       = df_arb %>% filter(borrowed == 1),
  "Repeat (Arb)"    = df_arb %>% filter(idrssd %in% arb_repeat_ids),
  "New (Arb)"       = df_arb %>% filter(idrssd %in% arb_new_ids)
)

panB <- bind_rows(imap(panB_items, ~gap_stats(.x, .y)))

kbl(panB %>% select(-Group) %>% t() %>% as.data.frame() %>%
      setNames(panB$Group) %>% tibble::rownames_to_column("Metric"),
    format = "html",
    caption = "Panel B: Liquidity Gap — Crisis vs Arbitrage (Repeat = also borrowed ≤May 4)") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 9) %>%
  footnote(general = "Repeat (Arb) = banks that borrowed DW/BTFP at any point through May 4, 2023
           AND also borrowed during the arbitrage window (Nov 15 – Jan 24).
           New (Arb) = banks that first used DW/BTFP only during the arbitrage window.
           Crisis variables measured at 2022Q4; Arb variables measured at 2023Q3.")
Panel B: Liquidity Gap — Crisis vs Arbitrage (Repeat = also borrowed ≤May 4)
Metric All (Crisis) Repeat (Crisis) New (Crisis) All (Arb) Repeat (Arb) New (Arb)
N 863 134 729 1023 458 565
Liq Gap ($000) 997166 602101 1069784 377906 450550 319019
Cash/D^U 0.374 0.153 0.414 0.311 0.281 0.336
SOMO_MV/DU 0.562 0.570 0.560 0.627 0.587 0.660
rho 0.936 0.722 0.975 0.939 0.868 0.996
rho_0 1.013 0.798 1.052 1.024 0.957 1.078
phi_decline 0.077 0.076 0.077 0.085 0.089 0.082
E^MV/A 270.33 272.60 269.92 NA NA NA
F/A 270.94 273.30 270.51 NA NA NA
F^U/A 10267.41 10554.59 10215.59 NA NA NA
v -9997.08 -10281.99 -9945.68 NA NA NA
Borrow ($000) 1376911099 2211193175 1223558839 225935860 288786011 174988305
Borrow Std ($000) 12124978444 5479857025 12978361745 1361138599 1174073557 1494808317
% Gap 78.2% 77.6% 78.3% 74.5% 77.5% 72.0%
Note:
Repeat (Arb) = banks that borrowed DW/BTFP at any point through May 4, 2023
AND also borrowed during the arbitrage window (Nov 15 – Jan 24).
New (Arb) = banks that first used DW/BTFP only during the arbitrage window.
Crisis variables measured at 2022Q4; Arb variables measured at 2023Q3.
# ══════════════════════════════════════════════════════════════════════════
# PANEL C: By sub-period (Pre, SVB week, BTFP active, FRC week) x type
# ══════════════════════════════════════════════════════════════════════════
cat("\n## Panel C: By Sub-Period × Borrower Type\n\n")

7.3 Panel C: By Sub-Period × Borrower Type

sub_period_ids <- list(
  "Pre-Crisis" = all_phases %>% filter(phase == "Pre-Crisis") %>% pull(idrssd),
  "SVB Week"   = all_phases %>% filter(phase == "P1_SVB") %>% pull(idrssd),
  "BTFP Active"= all_phases %>% filter(phase == "P2_BTFP") %>% pull(idrssd),
  "FRC Week"   = all_phases %>% filter(phase == "P3_FRC") %>% pull(idrssd)
)

panC_rows <- list()
for (period_nm in names(sub_period_ids)) {
  ids <- sub_period_ids[[period_nm]]
  panC_rows <- c(panC_rows, list(
    gap_stats(df %>% filter(idrssd %in% ids), paste0(period_nm, " — All")),
    gap_stats(df %>% filter(idrssd %in% ids & borrower_type == "DW Only"),
              paste0(period_nm, " — DW Only")),
    gap_stats(df %>% filter(idrssd %in% ids & borrower_type == "BTFP Only"),
              paste0(period_nm, " — BTFP Only")),
    gap_stats(df %>% filter(idrssd %in% ids & borrower_type == "Both"),
              paste0(period_nm, " — Both"))
  ))
}
panC <- bind_rows(panC_rows)

kbl(panC, format = "html", caption = "Panel C: Liquidity Gap by Sub-Period × Borrower Type") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 8) %>%
  footnote(general = "rho = (C + S^OMO_MV) / D^U. rho_0 = (C + S^OMO_book) / D^U (pre-shock).
           phi_decline = rho_0 - rho = rate-hike impact on coverage.
           Each column reports banks that borrowed from DW/BTFP in that sub-period.
           Banks can appear in multiple sub-periods if they borrowed in more than one.")
Panel C: Liquidity Gap by Sub-Period × Borrower Type
Group N Liq Gap ($000) Cash/D^U SOMO_MV/DU rho rho_0 phi_decline E^MV/A F/A F^U/A v Borrow ($000) Borrow Std ($000) % Gap
Pre-Crisis — All 260 137975 0.303 0.608 0.911 1.003 0.092 269.03 268.99 10222.94 -9953.91 1167427946 4075185318 78.1%
Pre-Crisis — DW Only 148 -386200 0.420 0.607 1.028 1.125 0.097 268.79 267.22 10450.14 -10181.35 503740403 1626525163 75.7%
Pre-Crisis — BTFP Only 0 NA NA NA NA NA NA NA NA NA NA NA NA NA
Pre-Crisis — Both 112 830634 0.148 0.609 0.757 0.842 0.085 269.35 271.39 9914.45 -9645.10 2044443627 5820908616 81.2%
SVB Week — All 68 2484745 0.143 0.426 0.569 0.635 0.066 249.60 250.11 9609.83 -9360.23 12067961795 41222462499 82.4%
SVB Week — DW Only 27 1755448 0.187 0.399 0.586 0.627 0.041 243.14 242.47 8990.27 -8747.13 1861250389 2931383818 77.8%
SVB Week — BTFP Only 0 NA NA NA NA NA NA NA NA NA NA NA NA NA
SVB Week — Both 41 2965014 0.114 0.444 0.558 0.640 0.082 253.86 255.13 10017.83 -9763.97 18789454672 52192914150 85.4%
BTFP Active — All 769 1022070 0.388 0.564 0.952 1.031 0.079 272.97 273.65 10483.44 -10210.46 1531834675 12836541994 78.7%
BTFP Active — DW Only 220 561575 0.842 0.566 1.407 1.490 0.083 281.64 280.85 11062.52 -10780.88 492763245 1951538752 74.5%
BTFP Active — BTFP Only 242 134909 0.235 0.590 0.825 0.892 0.067 255.35 256.75 8835.52 -8580.16 355615262 2574408448 78.5%
BTFP Active — Both 307 2051391 0.182 0.543 0.725 0.811 0.086 280.72 281.89 11376.65 -11095.93 3203628853 20023427148 81.8%
FRC Week — All 269 660268 0.199 0.526 0.725 0.792 0.066 268.47 269.40 9853.98 -9585.51 1526848895 4740523526 79.6%
FRC Week — DW Only 53 710467 0.299 0.390 0.690 0.734 0.044 240.86 239.25 8572.48 -8331.62 1748299947 3657795282 73.6%
FRC Week — BTFP Only 104 180421 0.197 0.557 0.754 0.817 0.062 283.10 284.60 10287.40 -10004.30 212995800 600666822 79.8%
FRC Week — Both 112 1082084 0.154 0.561 0.715 0.797 0.081 267.94 269.54 10059.78 -9791.84 2642061540 6690273261 82.1%
Note:
rho = (C + S^OMO_MV) / D^U. rho_0 = (C + S^OMO_book) / D^U (pre-shock).
phi_decline = rho_0 - rho = rate-hike impact on coverage.
Each column reports banks that borrowed from DW/BTFP in that sub-period.
Banks can appear in multiple sub-periods if they borrowed in more than one.

8 TABLE 4: DOUBLE-SORT Pr(Borrow) BY BANK SIZE

Recreates Table 3 Panel B: 3×3 double sort by ℓ × β^U, stratified by bank size (Large >$10B, Medium $1–10B, Small <$1B).

Note on BHC restriction: The public_bank_flag.csv only covers publicly traded banks (~366 of 4,700), making a BHC-only restriction impractical for size splits. We use all banks. If you have a separate BHC identifier from the call report (e.g., rssd_top for all banks, not just public), merge it before filtering.

# ══════════════════════════════════════════════════════════════════════════
# REUSABLE: Build 3×3 double-sort table for a given subset
# ══════════════════════════════════════════════════════════════════════════
build_double_sort <- function(data, title_str) {
  ds <- data %>%
    filter(!is.na(ell), !is.na(beta_u_clipped)) %>%
    mutate(ell_t = ntile(ell, 3), beta_t = ntile(beta_u_clipped, 3))

  if (nrow(ds) < 30) {
    cat(sprintf("\n**%s**: Too few observations (N=%d) for a 3×3 sort.\n\n",
                title_str, nrow(ds)))
    return(invisible(NULL))
  }

  ds_cells <- ds %>%
    group_by(ell_t, beta_t) %>%
    summarise(pr = mean(borrowed), N = n(), .groups = "drop")

  t3_wide <- ds_cells %>%
    mutate(cell = sprintf("%.1f%% [%s]", 100*pr, fmt(N))) %>%
    select(ell_t, beta_t, cell) %>%
    pivot_wider(names_from = beta_t, values_from = cell) %>% arrange(ell_t)

  # Row marginals (by loss tercile)
  ell_marg <- ds %>% group_by(ell_t) %>%
    summarise(pr = mean(borrowed), N = n(), .groups="drop") %>%
    mutate(cell = sprintf("%.1f%% [%s]", 100*pr, fmt(N))) %>% pull(cell)
  t3_wide$All <- ell_marg

  # Column marginals (by beta tercile)
  beta_marg <- ds %>% group_by(beta_t) %>%
    summarise(pr = mean(borrowed), N = n(), .groups="drop") %>%
    mutate(cell = sprintf("%.1f%% [%s]", 100*pr, fmt(N))) %>% pull(cell)
  all_marg <- sprintf("%.1f%% [%s]", 100*mean(ds$borrowed), fmt(nrow(ds)))

  t3_wide$ell_t <- c("Low ell (T1)", "Mid ell (T2)", "High ell (T3)")
  marg_row <- tibble(ell_t = "All",
                     `1` = beta_marg[1], `2` = beta_marg[2],
                     `3` = beta_marg[3], All = all_marg)
  t3_final <- bind_rows(t3_wide, marg_row)

  kbl(t3_final, format = "html",
      col.names = c("", "Low Beta^U (T1)", "Mid Beta^U (T2)",
                     "High Beta^U (T3)", "All"),
      caption = title_str) %>%
    kable_styling(bootstrap_options = c("striped","condensed"),
                  full_width = FALSE, font_size = 11) %>%
    footnote(general = sprintf(
      "Cell = Pr(Borrow) [N]. Terciles computed WITHIN this subset (N=%d).
       Theory predicts Pr(Borrow) increases toward the upper-right corner
       (high loss × high beta = low franchise buffer).", nrow(ds)))
}

# ══════════════════════════════════════════════════════════════════════════
# Size splits — use ALL banks (BHC flag covers only public banks)
# ══════════════════════════════════════════════════════════════════════════
n_bhc <- sum(df$is_bhc == 1, na.rm = TRUE)
cat(sprintf("Banks flagged as BHC (public_bank_flag): %d of %d (%.1f%%)\n\n",
            n_bhc, nrow(df), 100*n_bhc/nrow(df)))

Banks flagged as BHC (public_bank_flag): 374 of 4700 (8.0%)

cat(sprintf("Size distribution: Small=%s, Medium=%s, Large=%s\n\n",
            fmt(sum(df$size_bin == "Small (<$1B)")),
            fmt(sum(df$size_bin == "Medium ($1-10B)")),
            fmt(sum(df$size_bin == "Large (>$10B)"))))

Size distribution: Small=3,743, Medium=819, Large=138

cat("**Using all banks for size splits** (BHC flag only covers public banks).\n\n")

Using all banks for size splits (BHC flag only covers public banks).

cat("## Full Sample (All Banks)\n\n")

8.1 Full Sample (All Banks)

build_double_sort(df, "Full Sample — Pr(Borrow) by ell × Beta^U")
Full Sample — Pr(Borrow) by ell × Beta^U
Low Beta^U (T1) Mid Beta^U (T2) High Beta^U (T3) All
Low ell (T1) 33.3% [493] 34.1% [463] 42.6% [580] 37.0% [1,536]
Mid ell (T2) 46.5% [505] 48.4% [502] 57.6% [528] 50.9% [1,535]
High ell (T3) 54.3% [538] 50.0% [570] 52.9% [427] 52.3% [1,535]
All 45.0% [1,536] 44.7% [1,535] 50.6% [1,535] 46.8% [4,606]
Note:
Cell = Pr(Borrow) [N]. Terciles computed WITHIN this subset (N=4606).
Theory predicts Pr(Borrow) increases toward the upper-right corner
(high loss × high beta = low franchise buffer).
cat("\n## Large Banks (>$10B)\n\n")

8.2 Large Banks (>$10B)

build_double_sort(df %>% filter(size_bin == "Large (>$10B)"),
                  "Large Banks (>$10B): Pr(Borrow) by ell × Beta^U")
Large Banks (>$10B): Pr(Borrow) by ell × Beta^U
Low Beta^U (T1) Mid Beta^U (T2) High Beta^U (T3) All
Low ell (T1) 90.9% [11] 81.8% [11] 95.8% [24] 91.3% [46]
Mid ell (T2) 78.6% [14] 83.3% [18] 85.7% [14] 82.6% [46]
High ell (T3) 95.2% [21] 100.0% [17] 100.0% [7] 97.8% [45]
All 89.1% [46] 89.1% [46] 93.3% [45] 90.5% [137]
Note:
Cell = Pr(Borrow) [N]. Terciles computed WITHIN this subset (N=137).
Theory predicts Pr(Borrow) increases toward the upper-right corner
(high loss × high beta = low franchise buffer).
cat("\n## Medium Banks ($1-10B)\n\n")

8.3 Medium Banks ($1-10B)

build_double_sort(df %>% filter(size_bin == "Medium ($1-10B)"),
                  "Medium Banks ($1-10B): Pr(Borrow) by ell × Beta^U")
Medium Banks ($1-10B): Pr(Borrow) by ell × Beta^U
Low Beta^U (T1) Mid Beta^U (T2) High Beta^U (T3) All
Low ell (T1) 54.1% [74] 71.6% [74] 74.0% [123] 67.9% [271]
Mid ell (T2) 78.9% [90] 70.8% [89] 72.5% [91] 74.1% [270]
High ell (T3) 82.2% [107] 86.9% [107] 75.0% [56] 82.6% [270]
All 73.4% [271] 77.4% [270] 73.7% [270] 74.8% [811]
Note:
Cell = Pr(Borrow) [N]. Terciles computed WITHIN this subset (N=811).
Theory predicts Pr(Borrow) increases toward the upper-right corner
(high loss × high beta = low franchise buffer).
cat("\n## Small Banks (<$1B)\n\n")

8.4 Small Banks (<$1B)

build_double_sort(df %>% filter(size_bin == "Small (<$1B)"),
                  "Small Banks (<$1B): Pr(Borrow) by ell × Beta^U")
Small Banks (<$1B): Pr(Borrow) by ell × Beta^U
Low Beta^U (T1) Mid Beta^U (T2) High Beta^U (T3) All
Low ell (T1) 29.5% [407] 28.0% [375] 27.4% [438] 28.3% [1,220]
Mid ell (T2) 41.0% [400] 42.9% [385] 48.6% [434] 44.3% [1,219]
High ell (T3) 47.2% [413] 43.8% [459] 40.9% [347] 44.1% [1,219]
All 39.3% [1,220] 38.6% [1,219] 38.8% [1,219] 38.9% [3,658]
Note:
Cell = Pr(Borrow) [N]. Terciles computed WITHIN this subset (N=3658).
Theory predicts Pr(Borrow) increases toward the upper-right corner
(high loss × high beta = low franchise buffer).
# ── Identify BHC banks ──
bhc_ids <- bhc_flag %>% pull(idrssd) %>% unique()
df_bhc      <- df     %>% filter(idrssd %in% bhc_ids)
df_arb_bhc  <- df_arb %>% filter(idrssd %in% bhc_ids)

cat(sprintf("**BHC universe (from public_bank_flag):** %d banks in crisis panel, %d in arb panel\n\n",
            nrow(df_bhc), nrow(df_arb_bhc)))

BHC universe (from public_bank_flag): 374 banks in crisis panel, 359 in arb panel

cat(sprintf("BHC size distribution: Large=%d, Medium=%d, Small=%d\n\n",
            sum(df_bhc$size_bin == "Large (>$10B)"),
            sum(df_bhc$size_bin == "Medium ($1-10B)"),
            sum(df_bhc$size_bin == "Small (<$1B)")))

BHC size distribution: Large=99, Medium=237, Small=38

# ── Reuse gap_stats function (defined in Table 3 chunk) ──

# ══════════════════════════════════════════════════════════════════════════
# Helper: Build a full Panel-B-style table for a given subset
# ══════════════════════════════════════════════════════════════════════════
build_gap_panel_b <- function(df_crisis, df_arb_panel, subset_label,
                               ever_crisis_ids, arb_ids,
                               crisis_returning_ids, crisis_new_ids) {
  # Crisis borrowers in this subset
  crisis_borr <- df_crisis %>% filter(in_crisis == 1, borrowed == 1)
  crisis_ret  <- df_crisis %>% filter(idrssd %in% crisis_returning_ids)
  crisis_new  <- df_crisis %>% filter(idrssd %in% crisis_new_ids)

  # Arb borrowers in this subset
  arb_borr <- df_arb_panel %>% filter(borrowed == 1)
  arb_rep  <- df_arb_panel %>% filter(idrssd %in% intersect(arb_ids, ever_crisis_ids))
  arb_new  <- df_arb_panel %>% filter(idrssd %in% setdiff(arb_ids, ever_crisis_ids))

  items <- list(
    "All (Crisis)"    = crisis_borr,
    "Repeat (Crisis)" = crisis_ret,
    "New (Crisis)"    = crisis_new,
    "All (Arb)"       = arb_borr,
    "Repeat (Arb)"    = arb_rep,
    "New (Arb)"       = arb_new
  )

  tbl <- bind_rows(imap(items, ~gap_stats(.x, .y)))

  # Transpose for readability
  tbl_t <- tbl %>%
    select(-Group) %>% t() %>% as.data.frame() %>%
    setNames(tbl$Group) %>%
    tibble::rownames_to_column("Metric")

  kbl(tbl_t, format = "html",
      caption = sprintf("%s — Liquidity Gap: Crisis vs Arbitrage (BHC Only)", subset_label)) %>%
    kable_styling(bootstrap_options = c("striped","condensed"),
                  full_width = FALSE, font_size = 9) %>%
    footnote(general = "Repeat (Crisis) = pre-crisis DW user who also borrowed Mar 8–May 4.
             New (Crisis) = first-time borrower during crisis.
             Repeat (Arb) = borrowed at any point ≤May 4 AND during arb window.
             New (Arb) = first-time borrower only during arb window.
             Crisis vars at 2022Q4; Arb vars at 2023Q3.")
}

# ── Precompute arb classification IDs (same as Panel B) ──
# These should already exist from Table 3; redefine for safety
crisis_dw_all <- dw_loans %>%
  filter(dw_loan_date <= CRISIS_END) %>% pull(rssd_id) %>% unique()
crisis_btfp_all <- btfp_loans %>%
  filter(btfp_loan_date <= CRISIS_END) %>% pull(rssd_id) %>% unique()
ever_crisis_borrower <- union(crisis_dw_all, crisis_btfp_all)
arb_borrower_ids     <- union(arb_dw_ids, arb_btfp_ids)

# ══════════════════════════════════════════════════════════════════════════
# Panel by size
# ══════════════════════════════════════════════════════════════════════════

cat("## All BHC Banks\n\n")

8.5 All BHC Banks

build_gap_panel_b(df_bhc, df_arb_bhc, "All BHC Banks",
                  ever_crisis_borrower, arb_borrower_ids,
                  crisis_returning, crisis_new)
All BHC Banks — Liquidity Gap: Crisis vs Arbitrage (BHC Only)
Metric All (Crisis) Repeat (Crisis) New (Crisis) All (Arb) Repeat (Arb) New (Arb)
N 123 25 98 164 91 73
Liq Gap ($000) 4840653 1760032 5626526 1775785 1676789 1899190
Cash/D^U 0.109 0.083 0.116 0.264 0.237 0.297
SOMO_MV/DU 0.396 0.624 0.338 0.631 0.562 0.717
rho 0.506 0.707 0.454 0.895 0.798 1.015
rho_0 0.585 0.813 0.527 1.010 0.915 1.129
phi_decline 0.079 0.106 0.072 0.115 0.116 0.114
E^MV/A 333.72 300.60 342.17 NA NA NA
F/A 333.04 301.15 341.18 NA NA NA
F^U/A 15677.51 13100.28 16334.97 NA NA NA
v -15343.79 -12799.68 -15992.80 NA NA NA
Borrow ($000) 6890184022 4141564398 7591362497 658965694 585465612 750589082
Borrow Std ($000) 31057667469 7844550423 34576176258 2939644329 1647342995 4019440599
% Gap 95.1% 96.0% 94.9% 88.4% 91.2% 84.9%
Note:
Repeat (Crisis) = pre-crisis DW user who also borrowed Mar 8–May 4.
New (Crisis) = first-time borrower during crisis.
Repeat (Arb) = borrowed at any point ≤May 4 AND during arb window.
New (Arb) = first-time borrower only during arb window.
Crisis vars at 2022Q4; Arb vars at 2023Q3.
cat("\n## Large BHC Banks (>$10B)\n\n")

8.6 Large BHC Banks (>$10B)

build_gap_panel_b(
  df_bhc %>% filter(size_bin == "Large (>$10B)"),
  df_arb_bhc %>% filter(size_bin == "Large (>$10B)"),
  "Large BHC (>$10B)",
  ever_crisis_borrower, arb_borrower_ids,
  crisis_returning, crisis_new)
Large BHC (>$10B) — Liquidity Gap: Crisis vs Arbitrage (BHC Only)
Metric All (Crisis) Repeat (Crisis) New (Crisis) All (Arb) Repeat (Arb) New (Arb)
N 50 7 43 47 27 20
Liq Gap ($000) 10693889 4047551 11775851 4835399 4288838 5573256
Cash/D^U 0.096 0.053 0.103 0.438 0.272 0.663
SOMO_MV/DU 0.303 0.319 0.301 0.872 0.405 1.503
rho 0.400 0.373 0.404 1.310 0.677 2.166
rho_0 0.476 0.448 0.481 1.442 0.782 2.332
phi_decline 0.076 0.076 0.077 0.131 0.105 0.166
E^MV/A 365.40 297.67 376.43 NA NA NA
F/A 363.40 296.87 374.23 NA NA NA
F^U/A 18530.46 13996.49 19268.55 NA NA NA
v -18165.06 -13698.82 -18892.12 NA NA NA
Borrow ($000) 14699694315 8564358571 15698469901 1788484973 1474599435 2212230450
Borrow Std ($000) 47705230282 12960495323 51223000554 5323044910 2801554023 7584899966
% Gap 96.0% 100.0% 95.3% 83.0% 85.2% 80.0%
Note:
Repeat (Crisis) = pre-crisis DW user who also borrowed Mar 8–May 4.
New (Crisis) = first-time borrower during crisis.
Repeat (Arb) = borrowed at any point ≤May 4 AND during arb window.
New (Arb) = first-time borrower only during arb window.
Crisis vars at 2022Q4; Arb vars at 2023Q3.
cat("\n## Medium BHC Banks ($1-10B)\n\n")

8.7 Medium BHC Banks ($1-10B)

build_gap_panel_b(
  df_bhc %>% filter(size_bin == "Medium ($1-10B)"),
  df_arb_bhc %>% filter(size_bin == "Medium ($1-10B)"),
  "Medium BHC ($1-10B)",
  ever_crisis_borrower, arb_borrower_ids,
  crisis_returning, crisis_new)
Medium BHC ($1-10B) — Liquidity Gap: Crisis vs Arbitrage (BHC Only)
Metric All (Crisis) Repeat (Crisis) New (Crisis) All (Arb) Repeat (Arb) New (Arb)
N 67 14 53 110 61 49
Liq Gap ($000) 901325 1103213 847996 577887 604495 544763
Cash/D^U 0.115 0.075 0.126 0.176 0.187 0.163
SOMO_MV/DU 0.334 0.235 0.361 0.392 0.360 0.433
rho 0.449 0.309 0.486 0.569 0.547 0.596
rho_0 0.515 0.362 0.555 0.661 0.635 0.694
phi_decline 0.065 0.052 0.069 0.092 0.087 0.098
E^MV/A 317.71 319.35 317.28 NA NA NA
F/A 317.88 320.71 317.14 NA NA NA
F^U/A 14160.47 13733.28 14273.31 NA NA NA
v -13842.75 -13413.92 -13956.03 NA NA NA
Borrow ($000) 1649038209 2976874497 1298289000 210095491 207717623 213055694
Borrow Std ($000) 3925469816 4466132788 3737820773 438341300 403854947 482115434
% Gap 95.5% 100.0% 94.3% 91.8% 96.7% 85.7%
Note:
Repeat (Crisis) = pre-crisis DW user who also borrowed Mar 8–May 4.
New (Crisis) = first-time borrower during crisis.
Repeat (Arb) = borrowed at any point ≤May 4 AND during arb window.
New (Arb) = first-time borrower only during arb window.
Crisis vars at 2022Q4; Arb vars at 2023Q3.
cat("\n## Small BHC Banks (<$1B)\n\n")

8.8 Small BHC Banks (<$1B)

build_gap_panel_b(
  df_bhc %>% filter(size_bin == "Small (<$1B)"),
  df_arb_bhc %>% filter(size_bin == "Small (<$1B)"),
  "Small BHC (<$1B)",
  ever_crisis_borrower, arb_borrower_ids,
  crisis_returning, crisis_new)
Small BHC (<$1B) — Liquidity Gap: Crisis vs Arbitrage (BHC Only)
Metric All (Crisis) Repeat (Crisis) New (Crisis) All (Arb) Repeat (Arb) New (Arb)
N 6 4 2 7 3 4
Liq Gap ($000) 52853 55744 47072 56767 -28346 120602
Cash/D^U 0.154 0.164 0.136 0.464 0.924 0.118
SOMO_MV/DU 1.866 2.522 0.554 2.765 6.081 0.279
rho 2.020 2.686 0.690 3.229 7.005 0.397
rho_0 2.280 3.033 0.774 3.595 7.807 0.437
phi_decline 0.260 0.347 0.085 0.366 0.801 0.040
E^MV/A 248.40 240.07 265.05 NA NA NA
F/A 249.30 240.16 267.57 NA NA NA
F^U/A 8843.22 9316.40 7896.86 NA NA NA
v -8594.83 -9076.33 -7631.82 NA NA NA
Borrow ($000) 337059833 478089250 55001000 128725143 264137000 27166250
Borrow Std ($000) 275665690 212317614 77781746 145247946 112011518 41724039
% Gap 83.3% 75.0% 100.0% 71.4% 33.3% 100.0%
Note:
Repeat (Crisis) = pre-crisis DW user who also borrowed Mar 8–May 4.
New (Crisis) = first-time borrower during crisis.
Repeat (Arb) = borrowed at any point ≤May 4 AND during arb window.
New (Arb) = first-time borrower only during arb window.
Crisis vars at 2022Q4; Arb vars at 2023Q3.

9 TABLE 5: THEORY-PREDICTED REGIONS

How many banks fall into each theory region under different franchise calibrations? Stratified by bank size.

# ══════════════════════════════════════════════════════════════════════════
# REUSABLE: Region counts for a given subset
# ══════════════════════════════════════════════════════════════════════════
region_counts <- function(data, label = "") {
  d <- data %>% filter(!is.na(v_pp), !is.na(emv_pp))
  n_total <- nrow(d)
  if (n_total == 0) return(tibble(Group = label))
  
  tibble(
    Group = label,
    N = n_total,
    # (A) Using MTM only (no franchise): E - ell
    `MTM Only: Book Insolvent (E<lambda)` = sum(d$v_no_franchise < 0, na.rm=T),
    `MTM Only: % Book Insolvent` = sprintf("%.1f%%", 100*mean(d$v_no_franchise < 0, na.rm=T)),
    `MTM Only: Pr(Borrow|Insolvent)` = pct(d$borrowed[d$v_no_franchise < 0]),
    `MTM Only: Pr(Borrow|Solvent)` = pct(d$borrowed[d$v_no_franchise >= 0]),
    # (B) Using full deposit franchise (F on D)
    `F(D): Safe` = sum(d$region_B == "Safe", na.rm=T),
    `F(D): Panic` = sum(d$region_B == "Panic Zone", na.rm=T),
    `F(D): Insolvency` = sum(d$region_B == "Insolvency", na.rm=T),
    `F(D): Pr(B|Safe)` = pct(d$borrowed[d$region_B == "Safe"]),
    `F(D): Pr(B|Panic)` = pct(d$borrowed[d$region_B == "Panic Zone"]),
    `F(D): Pr(B|Insolv)` = if (sum(d$region_B == "Insolvency", na.rm=T) > 0) 
      pct(d$borrowed[d$region_B == "Insolvency"]) else "N/A",
    # (C) Using uninsured franchise only (F on D^U) — main spec
    `F(D^U): Safe` = sum(d$region_C == "Safe", na.rm=T),
    `F(D^U): Panic` = sum(d$region_C == "Panic Zone", na.rm=T),
    `F(D^U): Insolvency` = sum(d$region_C == "Insolvency", na.rm=T),
    `F(D^U): Pr(B|Safe)` = pct(d$borrowed[d$region_C == "Safe"]),
    `F(D^U): Pr(B|Panic)` = pct(d$borrowed[d$region_C == "Panic Zone"]),
    `F(D^U): Pr(B|Insolv)` = if (sum(d$region_C == "Insolvency", na.rm=T) > 0)
      pct(d$borrowed[d$region_C == "Insolvency"]) else "N/A"
  )
}

cat("## Panel A: Full Sample\n\n")

9.1 Panel A: Full Sample

reg_full <- bind_rows(
  region_counts(df, "All Banks"),
  region_counts(df %>% filter(size_bin == "Large (>$10B)"), "Large (>$10B)"),
  region_counts(df %>% filter(size_bin == "Medium ($1-10B)"), "Medium ($1-10B)"),
  region_counts(df %>% filter(size_bin == "Small (<$1B)"), "Small (<$1B)")
)

# Transpose for readability
reg_full_t <- reg_full %>% select(-Group) %>% t() %>% as.data.frame() %>%
  setNames(reg_full$Group) %>% tibble::rownames_to_column("Metric")

kbl(reg_full_t, format = "html",
    caption = "Theory-Predicted Regions by Franchise Calibration and Bank Size") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 9) %>%
  footnote(general = "Three calibrations: (A) No franchise (MTM only): E-ell < 0 = insolvent.
           (B) Full deposit franchise F on D. (C) Uninsured franchise F on D^U (main spec).
           Safe: v>=0. Panic: v<0, E^MV>0. Insolvency: E^MV<=0.")
Theory-Predicted Regions by Franchise Calibration and Bank Size
Metric All Banks Large (>$10B) Medium ($1-10B) Small (<$1B)
N 4606 137 811 3658
MTM Only: Book Insolvent (E<lambda) 1995 45 351 1599
MTM Only: % Book Insolvent 43.3% 32.8% 43.3% 43.7%
MTM Only: Pr(Borrow|Insolvent) 54.2% 93.3% 80.1% 47.4%
MTM Only: Pr(Borrow|Solvent) 41.1% 89.1% 70.9% 32.3%
F(D): Safe 68 13 20 35
F(D): Panic 4531 123 788 3620
F(D): Insolvency 7 1 3 3
F(D): Pr(B|Safe) 57.4% 92.3% 85.0% 28.6%
F(D): Pr(B|Panic) 46.6% 90.2% 74.7% 39.0%
F(D): Pr(B|Insolv) 42.9% 100.0% 33.3% 33.3%
F(D^U): Safe 71 12 22 37
F(D^U): Panic 4522 123 786 3613
F(D^U): Insolvency 13 2 3 8
F(D^U): Pr(B|Safe) 54.9% 91.7% 86.4% 24.3%
F(D^U): Pr(B|Panic) 46.7% 90.2% 74.7% 39.1%
F(D^U): Pr(B|Insolv) 38.5% 100.0% 33.3% 25.0%
Note:
Three calibrations: (A) No franchise (MTM only): E-ell < 0 = insolvent.
(B) Full deposit franchise F on D. (C) Uninsured franchise F on D^U (main spec).
Safe: v>=0. Panic: v<0, E^MV>0. Insolvency: E^MV<=0.
# ── Cross-tab: B vs C ──
cat("\n## Panel B: Region Migration (F on D → F on D^U)\n\n")

9.2 Panel B: Region Migration (F on D → F on D^U)

cross_tab <- df %>%
  filter(!is.na(region_B), !is.na(region_C)) %>%
  count(region_B, region_C) %>%
  pivot_wider(names_from = region_C, values_from = n, values_fill = 0)

kbl(cross_tab, format = "html",
    caption = "Region Migration: Full Deposit Franchise (rows) → Uninsured Only (cols)") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE) %>%
  footnote(general = "Shows how banks reclassify when moving from F on D to F on D^U.")
Region Migration: Full Deposit Franchise (rows) → Uninsured Only (cols)
region_B Safe Insolvency Panic Zone
Safe 62 6 0
Panic Zone 9 0 4522
Insolvency 0 7 0
Note:
Shows how banks reclassify when moving from F on D to F on D^U.

10 TABLE 6: REGRESSION ANALYSIS

Three predictions from theory (pp. 7-8). All continuous regressors z-standardized. HC1 robust SEs throughout.

# ══════════════════════════════════════════════════════════════════════════
# REGRESSION SAMPLE
# ══════════════════════════════════════════════════════════════════════════
df_reg <- df %>%
  filter(!is.na(ell), !is.na(du_d), !is.na(phi), !is.na(ell_omo),
         !is.na(eq_ta), !is.na(cash_ta), !is.na(log_ta), !is.na(roa),
         !is.na(loan_to_deposit), !is.na(wholesale_ta)) %>%
  mutate(
    z_ell = z_std(ell), z_ell_omo = z_std(ell_omo),
    z_ell_nonomo = z_std(ell_nonomo), z_ell_loan = z_std(ell_loan),
    z_mu = z_std(du_d), z_rho = z_std(phi),
    z_ell_x_mu = z_ell * z_mu,
    z_ellomo_x_rho = z_ell_omo * z_rho,
    z_eq = z_std(eq_ta), z_cash = z_std(cash_ta), z_logta = z_std(log_ta),
    z_wholesale = z_std(wholesale_ta), z_roa = z_std(roa),
    z_loandep = z_std(loan_to_deposit),
    log_amt = ifelse(g > 0, log(g_000 + 1), NA_real_),
    chose_btfp = as.integer(used_btfp == 1 & used_dw == 0),
    chose_dw = as.integer(used_dw == 1 & used_btfp == 0),
    chose_both = as.integer(used_dw == 1 & used_btfp == 1)
  )

cat(sprintf("Regression sample: %s banks (%s borrowers, %s non-borrowers)\n",
    fmt(nrow(df_reg)), fmt(sum(df_reg$borrowed)), fmt(sum(1 - df_reg$borrowed))))

Regression sample: 4,626 banks (2,164 borrowers, 2,462 non-borrowers)

# ── Robust extraction helper ──
extract_robust <- function(mod, label, is_probit = FALSE) {
  if (is_probit) {
    cf <- coeftest(mod, vcov. = vcovHC(mod, type = "HC1"))
    xb <- predict(mod, type = "link"); aphi <- mean(dnorm(xb))
    est <- cf[,"Estimate"] * aphi; se <- cf[,"Std. Error"] * aphi
    tval <- est / se; pval <- 2 * pnorm(-abs(tval))
  } else {
    cf <- coeftest(mod, vcov. = vcovHC(mod, type = "HC1"))
    est <- cf[,"Estimate"]; se <- cf[,"Std. Error"]
    tval <- cf[,"t value"]; pval <- cf[,"Pr(>|t|)"]
  }
  stars <- case_when(pval < 0.01 ~ "***", pval < 0.05 ~ "**", pval < 0.10 ~ "*", TRUE ~ "")
  tibble(Model = label, Term = rownames(cf),
    Coef = round(est, 4), SE = round(se, 4), t = round(tval, 2), p = round(pval, 4),
    Stars = stars,
    Display = paste0(formatC(est, format = "f", digits = 4), stars),
    SE_disp = paste0("(", formatC(se, format = "f", digits = 4), ")"))
}

# ── Paper-style table builder ──
make_reg_table <- function(res_list, term_order) {
  all_res <- bind_rows(res_list); models <- unique(all_res$Model)
  map_dfr(term_order, function(tm) {
    cr <- tibble(Variable = tm); sr <- tibble(Variable = "")
    for (m in models) {
      r <- all_res %>% filter(Model == m, Term == tm)
      if (nrow(r) == 1) { cr[[m]] <- r$Display; sr[[m]] <- r$SE_disp
      } else { cr[[m]] <- ""; sr[[m]] <- "" }
    }
    bind_rows(cr, sr)
  })
}

ctrl_str <- "+ z_eq + z_cash + z_logta + z_wholesale + z_roa + z_loandep"

# ══════════════════════════════════════════════════════════════════════════
# PREDICTION 1: EXTENSIVE MARGIN — Who Borrows?
# ══════════════════════════════════════════════════════════════════════════
cat("## Prediction 1: Extensive Margin (Pr(Borrow))\n\n")

10.1 Prediction 1: Extensive Margin (Pr(Borrow))

m1a <- lm(as.formula(paste("borrowed ~ z_ell + z_mu + z_ell_x_mu", ctrl_str)), data = df_reg)
m1b <- lm(as.formula(paste("borrowed ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)), data = df_reg)
m1c <- lm(as.formula(paste("borrowed ~ z_ell_omo + z_ell_nonomo + z_ell_loan + z_mu + z_ell_x_mu + z_rho", ctrl_str)), data = df_reg)
m1d <- lm(as.formula(paste("borrowed ~ z_ell + z_mu + z_ell_x_mu + z_ellomo_x_rho", ctrl_str)), data = df_reg)
m1e <- glm(as.formula(paste("borrowed ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)),
           data = df_reg, family = binomial(link = "probit"))

expl1 <- c("z_ell","z_ell_omo","z_ell_nonomo","z_ell_loan","z_mu","z_ell_x_mu","z_rho","z_ellomo_x_rho")
ctrls <- c("z_eq","z_cash","z_logta","z_wholesale","z_roa","z_loandep","(Intercept)")

reg1 <- make_reg_table(
  list(extract_robust(m1a,"(1)"), extract_robust(m1b,"(2)"),
       extract_robust(m1c,"(3)"), extract_robust(m1d,"(4)"),
       extract_robust(m1e,"(5)", TRUE)),
  c(expl1, ctrls))

fit1 <- tibble(Variable = c("N", "N(Borrow=1)", "Mean(Y)", "R-sq", "Spec"),
  `(1)` = c(fmt(nobs(m1a)), fmt(sum(df_reg$borrowed)), fmt4(mean(df_reg$borrowed)),
            fmt4(summary(m1a)$r.squared), "Cond 1"),
  `(2)` = c(fmt(nobs(m1b)), fmt(sum(df_reg$borrowed)), fmt4(mean(df_reg$borrowed)),
            fmt4(summary(m1b)$r.squared), "Eq.18"),
  `(3)` = c(fmt(nobs(m1c)), fmt(sum(df_reg$borrowed)), fmt4(mean(df_reg$borrowed)),
            fmt4(summary(m1c)$r.squared), "Decomp"),
  `(4)` = c(fmt(nobs(m1d)), fmt(sum(df_reg$borrowed)), fmt4(mean(df_reg$borrowed)),
            fmt4(summary(m1d)$r.squared), "Eq.19"),
  `(5)` = c(fmt(nobs(m1e)), fmt(sum(df_reg$borrowed)), fmt4(mean(df_reg$borrowed)),
            fmt4(1-m1e$deviance/m1e$null.deviance), "Probit AME"))

kbl(bind_rows(reg1, fit1), format = "html", escape = FALSE,
    caption = "Prediction 1: Pr(Borrow) — Extensive Margin (HC1 SEs)") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("Theory Variables", 1, length(expl1)*2) %>%
  pack_rows("Controls", length(expl1)*2+1, (length(expl1)+length(ctrls))*2) %>%
  pack_rows("Fit", (length(expl1)+length(ctrls))*2+1, nrow(bind_rows(reg1, fit1))) %>%
  footnote(general = "Z-standardized. HC1 SEs. ***p<.01 **p<.05 *p<.10.
  (1) Cond 1 only. (2) Eq.18: both conditions. (3) Non-overlapping loss decomposition.
  (4) ell_OMO × rho compounding. (5) Probit marginal effects at mean.")
Prediction 1: Pr(Borrow) — Extensive Margin (HC1 SEs)
Variable
Theory Variables
z_ell 0.0414*** 0.0390*** 0.0414*** 0.0347***
(0.0108) (0.0109) (0.0108) (0.0112)
z_ell_omo 0.0135* 0.0265*** 0.0178**
(0.0075) (0.0074) (0.0087)
z_ell_nonomo 0.0363***
(0.0097)
z_ell_loan 0.0149
(0.0096)
z_mu 0.0102 0.0123 0.0103 0.0103 0.0081
(0.0077) (0.0076) (0.0077) (0.0077) (0.0083)
z_ell_x_mu -0.0100*** -0.0096*** -0.0098*** -0.0100*** -0.0075**
(0.0037) (0.0037) (0.0036) (0.0037) (0.0037)
z_rho 0.0144*** 0.0140*** 0.0190***
(0.0018) (0.0017) (0.0052)
z_ellomo_x_rho -0.0028
(0.0063)
Controls
z_eq -0.0263*** -0.0282*** -0.0282*** -0.0262*** -0.0656***
(0.0057) (0.0058) (0.0057) (0.0057) (0.0121)
z_cash -0.0389*** -0.0389*** -0.0332*** -0.0390*** -0.0438***
(0.0072) (0.0071) (0.0071) (0.0072) (0.0094)
z_logta 0.1893*** 0.1834*** 0.1843*** 0.1893*** 0.1925***
(0.0073) (0.0077) (0.0077) (0.0073) (0.0104)
z_wholesale 0.0175** 0.0160** 0.0134* 0.0175** 0.0100
(0.0074) (0.0074) (0.0073) (0.0074) (0.0076)
z_roa -0.0024 -0.0014 -0.0013 -0.0024 -0.0017
(0.0015) (0.0015) (0.0014) (0.0015) (0.0041)
z_loandep -0.0236*** -0.0167** -0.0029 -0.0239*** -0.0076
(0.0081) (0.0082) (0.0087) (0.0081) (0.0103)
(Intercept) 0.4670*** 0.4670*** 0.4670*** 0.4670*** -0.0322***
(0.0066) (0.0066) (0.0066) (0.0066) (0.0068)
Fit
N 4,626 4,626 4,626 4,626 4,626
N(Borrow=1) 2,164 2,164 2,164 2,164 2,164
Mean(Y) 0.4678 0.4678 0.4678 0.4678 0.4678
R-sq 0.1879 0.1893 0.1900 0.1879 0.1608
Spec Cond 1 Eq.18 Decomp Eq.19 Probit AME
Note:
Z-standardized. HC1 SEs. p<.01 p<.05 p<.10.
(1) Cond 1 only. (2) Eq.18: both conditions. (3) Non-overlapping loss decomposition.
(4) ell_OMO × rho compounding. (5) Probit marginal effects at mean.
# ══════════════════════════════════════════════════════════════════════════
# PREDICTION 2: INTENSIVE MARGIN — How Much?
# ══════════════════════════════════════════════════════════════════════════
cat("\n## Prediction 2: Intensive Margin (Amount | Borrow=1)\n\n")

10.2 Prediction 2: Intensive Margin (Amount | Borrow=1)

df_borr <- df_reg %>% filter(borrowed == 1, !is.na(log_amt)) %>% mutate(amt_w = winsorize(g_000))
m2a <- lm(as.formula(paste("log_amt ~ z_ell_omo", ctrl_str)), data = df_borr)
m2b <- lm(as.formula(paste("log_amt ~ z_ell_omo + z_ell_loan", ctrl_str)), data = df_borr)
m2c <- lm(as.formula(paste("log_amt ~ z_ell_omo + z_ell_loan + z_mu + z_rho", ctrl_str)), data = df_borr)
m2d <- lm(as.formula(paste("amt_w ~ z_ell_omo + z_ell_loan + z_mu + z_rho", ctrl_str)), data = df_borr)

expl2 <- c("z_ell_omo","z_ell_loan","z_mu","z_rho")
reg2 <- make_reg_table(
  list(extract_robust(m2a,"(1)"), extract_robust(m2b,"(2)"),
       extract_robust(m2c,"(3)"), extract_robust(m2d,"(4)")),
  c(expl2, ctrls))

fit2 <- tibble(Variable = c("N", "Mean(Y)", "R-sq", "DepVar"),
  `(1)` = c(fmt(nobs(m2a)), fmt4(mean(df_borr$log_amt, na.rm=T)),
            fmt4(summary(m2a)$r.squared), "log(Amt)"),
  `(2)` = c(fmt(nobs(m2b)), fmt4(mean(df_borr$log_amt, na.rm=T)),
            fmt4(summary(m2b)$r.squared), "log(Amt)"),
  `(3)` = c(fmt(nobs(m2c)), fmt4(mean(df_borr$log_amt, na.rm=T)),
            fmt4(summary(m2c)$r.squared), "log(Amt)"),
  `(4)` = c(fmt(nobs(m2d)), fmt4(mean(df_borr$amt_w, na.rm=T)),
            fmt4(summary(m2d)$r.squared), "Level (wins.)"))
kbl(bind_rows(reg2, fit2), format = "html", escape = FALSE,
    caption = "Prediction 2: Amount | Borrow=1 (HC1 SEs)") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("Theory", 1, length(expl2)*2) %>%
  pack_rows("Controls", length(expl2)*2+1, (length(expl2)+length(ctrls))*2) %>%
  pack_rows("Fit", (length(expl2)+length(ctrls))*2+1, nrow(bind_rows(reg2, fit2))) %>%
  footnote(general = "Borrowers only. d1>0 on ell_OMO: OMO determines facility capacity (Eq.20).")
Prediction 2: Amount | Borrow=1 (HC1 SEs)
Variable
Theory
z_ell_omo -0.0097 -0.0061 0.0038 7938.7644
(0.0948) (0.0953) (0.0942) (14721.2855)
z_ell_loan -0.1734 0.0550 2915.0402
(0.1721) (0.1757) (15843.7223)
z_mu 0.5328*** 23641.5236
(0.1139) (14776.8191)
z_rho 0.2341*** 2361.7675
(0.0642) (4385.6169)
Controls
z_eq 0.0814 0.0668 0.0462 22703.6603
(0.2078) (0.2072) (0.2068) (18741.4610)
z_cash -1.7815*** -1.7948*** -1.9909*** -58740.8868***
(0.1403) (0.1404) (0.1397) (13028.8064)
z_logta 0.5705*** 0.5581*** 0.3178** 165342.9486***
(0.1126) (0.1129) (0.1234) (18915.2766)
z_wholesale 0.6683*** 0.6651*** 0.6867*** 50884.5928***
(0.0977) (0.0978) (0.0982) (12121.7948)
z_roa 0.2081 0.1131 1.7234** 31412.1563
(1.0328) (1.0197) (0.8382) (64274.0523)
z_loandep -1.0710*** -1.0135*** -1.0769*** -79712.2165***
(0.1636) (0.1749) (0.1761) (20371.1350)
(Intercept) 7.1160*** 7.1136*** 7.1031*** 75474.9038***
(0.1003) (0.1004) (0.0977) (5930.3951)
Fit
N 2,164 2,164 2,164 2,164
Mean(Y) 7.7204 7.7204 7.7204 161,661.7946
R-sq 0.1225 0.1230 0.1342 0.1600
DepVar log(Amt) log(Amt) log(Amt) Level (wins.)
Note:
Borrowers only. d1>0 on ell_OMO: OMO determines facility capacity (Eq.20).
# ══════════════════════════════════════════════════════════════════════════
# PREDICTION 3: FACILITY CHOICE
# ══════════════════════════════════════════════════════════════════════════
cat("\n## Prediction 3: Facility Choice (Borrow=1)\n\n")

10.3 Prediction 3: Facility Choice (Borrow=1)

df_fac <- df_reg %>% filter(borrowed == 1)
m3a <- lm(as.formula(paste("chose_btfp ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)), data = df_fac)
m3b <- lm(as.formula(paste("chose_dw ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)), data = df_fac)
m3c <- lm(as.formula(paste("chose_both ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)), data = df_fac)

expl3 <- c("z_ell","z_ell_omo","z_mu","z_ell_x_mu","z_rho")
reg3 <- make_reg_table(
  list(extract_robust(m3a,"BTFP"), extract_robust(m3b,"DW"), extract_robust(m3c,"Both")),
  c(expl3, ctrls))

fit3 <- tibble(Variable = c("N", "N(Y=1)", "Mean(Y)", "R-sq"),
  BTFP = c(fmt(nobs(m3a)), fmt(sum(df_fac$chose_btfp)), fmt4(mean(df_fac$chose_btfp)),
           fmt4(summary(m3a)$r.squared)),
  DW   = c(fmt(nobs(m3b)), fmt(sum(df_fac$chose_dw)), fmt4(mean(df_fac$chose_dw)),
           fmt4(summary(m3b)$r.squared)),
  Both = c(fmt(nobs(m3c)), fmt(sum(df_fac$chose_both)), fmt4(mean(df_fac$chose_both)),
           fmt4(summary(m3c)$r.squared)))

kbl(bind_rows(reg3, fit3), format = "html", escape = FALSE,
    caption = "Prediction 3: Facility Choice | Borrow=1 (HC1 SEs)") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("Theory", 1, length(expl3)*2) %>%
  pack_rows("Controls", length(expl3)*2+1, (length(expl3)+length(ctrls))*2) %>%
  pack_rows("Fit", (length(expl3)+length(ctrls))*2+1, nrow(bind_rows(reg3, fit3))) %>%
  footnote(general = "LPM. ell×mu should predict BTFP (double-hit) and be attenuated for DW.")
Prediction 3: Facility Choice | Borrow=1 (HC1 SEs)
Variable BTFP DW Both
Theory
z_ell -0.0339 -0.0153 0.0492**
(0.0210) (0.0223) (0.0201)
z_ell_omo 0.0040 -0.0041 0.0001
(0.0109) (0.0115) (0.0112)
z_mu -0.0074 -0.0320** 0.0395***
(0.0109) (0.0125) (0.0113)
z_ell_x_mu -0.0311* 0.0003 0.0308*
(0.0160) (0.0184) (0.0162)
z_rho 0.0055 -0.0063 0.0008
(0.0046) (0.0050) (0.0046)
Controls
z_eq -0.0021 0.0403* -0.0382**
(0.0223) (0.0225) (0.0188)
z_cash -0.0989*** 0.1472*** -0.0483***
(0.0153) (0.0165) (0.0131)
z_logta -0.1249*** 0.0519*** 0.0730***
(0.0111) (0.0127) (0.0118)
z_wholesale 0.0271** -0.0485*** 0.0214**
(0.0118) (0.0110) (0.0105)
z_roa 0.0561 -0.0276 -0.0284
(0.0550) (0.0665) (0.0607)
z_loandep -0.0875*** 0.1050*** -0.0174
(0.0179) (0.0182) (0.0157)
(Intercept) 0.3742*** 0.4155*** 0.2103***
(0.0116) (0.0117) (0.0095)
Fit
N 2,164 2,164 2,164
N(Y=1) 728 859 577
Mean(Y) 0.3364 0.3970 0.2666
R-sq 0.1059 0.0847 0.0698
Note:
LPM. ell×mu should predict BTFP (double-hit) and be attenuated for DW.
# ══════════════════════════════════════════════════════════════════════════
# FALSIFICATION TESTS
# ══════════════════════════════════════════════════════════════════════════
cat("\n## Falsification: Arbitrage BTFP & FHLB\n\n")

10.4 Falsification: Arbitrage BTFP & FHLB

df_arb_reg <- df_arb %>%
  filter(!is.na(ell), !is.na(du_d), !is.na(phi), !is.na(ell_omo),
         !is.na(eq_ta), !is.na(cash_ta), !is.na(log_ta), !is.na(roa),
         !is.na(loan_to_deposit), !is.na(wholesale_ta)) %>%
  mutate(z_ell = z_std(ell), z_ell_omo = z_std(ell_omo), z_mu = z_std(du_d),
    z_rho = z_std(phi), z_eq = z_std(eq_ta), z_cash = z_std(cash_ta),
    z_logta = z_std(log_ta), z_wholesale = z_std(wholesale_ta),
    z_roa = z_std(roa), z_loandep = z_std(loan_to_deposit), z_ell_x_mu = z_ell * z_mu)

m_arb <- lm(as.formula(paste("borrowed ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)),
            data = df_arb_reg)

# FHLB (2023Q1)
call_2023q1 <- call_q %>% filter(period == "2023Q1", !idrssd %in% excluded) %>%
  left_join(dssw_beta_2022q4, by = "idrssd") %>%
  mutate(
    beta_u_clipped = pmin(pmax(ifelse(!is.na(beta_uninsured), beta_uninsured, NA_real_), 0), 1),
    du_d = safe_div(uninsured_deposit, insured_deposit + uninsured_deposit, NA_real_),
    ell = mtm_loss_to_total_asset, ell_omo = loss_omo_to_total_asset,
    eq_ta = book_equity_to_total_asset, cash_ta = cash_to_total_asset,
    log_ta = log(total_asset),
    wholesale_ta = 100 * (fhlb_adv + fed_fund_purchase + repo) / total_asset,
    D_U = replace_na(uninsured_deposit, 0), C = replace_na(cash, 0),
    omo_mv = pmax(omo_eligible - loss_omo_total, 0),
    phi = safe_div(C + omo_mv, D_U, NA_real_),
    fhlb_borrowed = as.integer(abnormal_fhlb_borrowing_10pct == 1)
  ) %>%
  filter(!is.na(ell), !is.na(du_d), !is.na(phi), !is.na(eq_ta), !is.na(cash_ta),
         !is.na(roa), !is.na(loan_to_deposit), !is.na(wholesale_ta)) %>%
  mutate(z_ell = z_std(ell), z_mu = z_std(du_d), z_rho = z_std(phi),
    z_ell_omo = z_std(ell_omo), z_eq = z_std(eq_ta), z_cash = z_std(cash_ta),
    z_logta = z_std(log_ta), z_wholesale = z_std(wholesale_ta),
    z_roa = z_std(roa), z_loandep = z_std(loan_to_deposit), z_ell_x_mu = z_ell * z_mu)

m_fhlb <- lm(as.formula(paste("fhlb_borrowed ~ z_ell + z_mu + z_ell_x_mu + z_rho + z_ell_omo", ctrl_str)),
             data = call_2023q1)

expl_f <- c("z_ell","z_ell_omo","z_mu","z_ell_x_mu","z_rho")
falsif <- make_reg_table(
  list(extract_robust(m_arb, "Arb BTFP"), extract_robust(m_fhlb, "FHLB")),
  c(expl_f, ctrls))

fitf <- tibble(Variable = c("N", "N(Y=1)", "Mean(Y)", "R-sq", "Baseline"),
  `Arb BTFP` = c(fmt(nobs(m_arb)), fmt(sum(df_arb_reg$borrowed)),
                 fmt4(mean(df_arb_reg$borrowed)),
                 fmt4(summary(m_arb)$r.squared), "2023Q3"),
  FHLB = c(fmt(nobs(m_fhlb)), fmt(sum(call_2023q1$fhlb_borrowed, na.rm=T)),
           fmt4(mean(call_2023q1$fhlb_borrowed, na.rm=T)),
           fmt4(summary(m_fhlb)$r.squared), "2022Q4"))

kbl(bind_rows(falsif, fitf), format = "html", escape = FALSE,
    caption = "Falsification: Arbitrage (2023Q3) & FHLB (2023Q1) — HC1 SEs") %>%
  kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("Theory", 1, length(expl_f)*2) %>%
  pack_rows("Controls", length(expl_f)*2+1, (length(expl_f)+length(ctrls))*2) %>%
  pack_rows("Fit", (length(expl_f)+length(ctrls))*2+1, nrow(bind_rows(falsif, fitf))) %>%
  footnote(general = "Arb: BTFP rate < IOER → carry trade; c3≈0. FHLB: loan-collateralized; c3,c5≈0.")
Falsification: Arbitrage (2023Q3) & FHLB (2023Q1) — HC1 SEs
Variable Arb BTFP FHLB
Theory
z_ell 0.0330*** 0.0100
(0.0097) (0.0063)
z_ell_omo 0.0103 -0.0081**
(0.0079) (0.0034)
z_mu 0.0071 0.0044
(0.0066) (0.0040)
z_ell_x_mu -0.0075*** -0.0016
(0.0027) (0.0019)
z_rho 0.0043 0.0016
(0.0031) (0.0015)
Controls
z_eq -0.0162*** -0.0034
(0.0051) (0.0027)
z_cash -0.0375*** -0.0259***
(0.0056) (0.0031)
z_logta 0.0979*** 0.0015
(0.0078) (0.0042)
z_wholesale 0.0018 -0.0079**
(0.0068) (0.0038)
z_roa -0.0004 0.0008
(0.0032) (0.0009)
z_loandep -0.0053 0.0194***
(0.0069) (0.0048)
(Intercept) 0.2256*** 0.0758***
(0.0059) (0.0039)
Fit
N 4,524 4,599
N(Y=1) 1,023 349
Mean(Y) 0.2261 0.0759
R-sq 0.0894 0.0194
Baseline 2023Q3 2022Q4
Note:
Arb: BTFP rate < IOER → carry trade; c3≈0. FHLB: loan-collateralized; c3,c5≈0.

11 COMPREHENSIVE BORROWER vs NON-BORROWER DESCRIPTIVE TABLE

Full descriptive summary (Crisis Period, March 8 – May 4, 2023): Compares Non-Borrowers with All Borrowers, BTFP-Only, DW-Only, and Both-facility borrowers. Borrower status is defined by whether a bank borrowed from DW or BTFP during the crisis window only. Balance-sheet variables are measured at 2022Q4 (pre-crisis baseline). T-statistics test the null that each borrower group mean equals the non-borrower mean.

# ══════════════════════════════════════════════════════════════════════════
# STEP 0: Re-aggregate borrowing for CRISIS PERIOD ONLY (Mar 8 – May 4)
# ══════════════════════════════════════════════════════════════════════════

crisis_dw_bank <- dw_loans %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= CRISIS_END) %>%
  group_by(rssd_id) %>%
  summarise(dw_total = sum(dw_loan_amount), dw_n = n(),
    dw_mean_rate = mean(dw_interest_rate, na.rm=T),
    dw_mean_term = mean(dw_term, na.rm=T),
    dw_mean_effmat = mean(dw_effective_maturity_days, na.rm=T),
    .groups = "drop") %>% rename(idrssd = rssd_id)

crisis_btfp_bank <- btfp_loans %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= CRISIS_END) %>%
  group_by(rssd_id) %>%
  summarise(btfp_total = sum(btfp_loan_amount), btfp_n = n(),
    btfp_mean_rate = mean(btfp_interest_rate, na.rm=T),
    btfp_mean_term = mean(btfp_term, na.rm=T),
    btfp_mean_effmat = mean(btfp_effective_maturity_days, na.rm=T),
    .groups = "drop") %>% rename(idrssd = rssd_id)

# Build crisis-specific panel from 2022Q4 call report
df_desc <- call_2022q4 %>%
  filter(!idrssd %in% excluded) %>%
  left_join(dssw_beta_2022q4, by = "idrssd") %>%
  { if (HAS_COSTS) left_join(., deposit_costs_2022q4, by = "idrssd") else . } %>%
  left_join(public_flag %>% filter(period == "2022Q4") %>% select(idrssd, is_public),
            by = "idrssd") %>%
  mutate(is_public = replace_na(is_public, 0L)) %>%
  left_join(crisis_dw_bank, by = "idrssd") %>%
  left_join(crisis_btfp_bank, by = "idrssd") %>%
  mutate(
    dw_total   = replace_na(dw_total, 0),
    btfp_total = replace_na(btfp_total, 0),
    dw_n   = replace_na(dw_n, 0L),
    btfp_n = replace_na(btfp_n, 0L),
    g = dw_total + btfp_total, g_000 = g / 1000,
    used_dw   = as.integer(dw_total > 0),
    used_btfp = as.integer(btfp_total > 0),
    borrower_type_crisis = factor(case_when(
      used_dw == 1 & used_btfp == 1 ~ "Both",
      used_dw == 1 ~ "DW Only",
      used_btfp == 1 ~ "BTFP Only",
      TRUE ~ "Non-Borrower"),
      levels = c("Non-Borrower", "DW Only", "BTFP Only", "Both")),
    borrowed_crisis = as.integer(g > 0)
  )

# Add all the structural / derived variables needed for the table
df_desc <- df_desc %>%
  mutate(
    beta_u_clipped = pmin(pmax(ifelse(!is.na(beta_uninsured), beta_uninsured, NA_real_), 0), 1),
    cost_u = if ("deposit_cost_uninsured" %in% names(.)) {
      ifelse(!is.na(deposit_cost_uninsured), deposit_cost_uninsured, 0)
    } else 0,
    du_ta = uninsured_deposit / total_asset,
    d_ta  = (insured_deposit + uninsured_deposit) / total_asset,
    du_d  = safe_div(uninsured_deposit, insured_deposit + uninsured_deposit, NA_real_),
    di_d  = safe_div(insured_deposit, insured_deposit + uninsured_deposit, NA_real_),
    gross_rent = (1 - beta_u_clipped) * y_10yr,
    net_rent   = gross_rent - cost_u,
    f_pp  = ifelse(!is.na(beta_u_clipped), pmax(net_rent * cap_factor * du_ta, 0) * 100, NA_real_),
    fu_pp = ifelse(!is.na(f_pp), du_d * f_pp, NA_real_),
    fi_pp = ifelse(!is.na(f_pp), di_d * f_pp, NA_real_),
    ell        = mtm_loss_to_total_asset,
    ell_sec    = loss_total_sec_to_total_asset,
    ell_omo    = loss_omo_to_total_asset,
    ell_loan   = loss_total_loan_to_total_asset,
    ell_nonomo = loss_nonomo_sec_to_total_asset,
    eq_ta    = book_equity_to_total_asset,
    cash_ta  = cash_to_total_asset,
    sec_ta   = security_to_total_asset,
    omo_ta   = omo_eligible_to_total_asset,
    dep_ta   = total_deposit_to_total_asset,
    uid_ta   = uninsured_deposit_to_total_asset,
    log_ta   = log(total_asset),
    loan_ta  = total_loan_to_total_asset,
    fhlb_ta  = fhlb_to_total_asset,
    one_minus_beta = 1 - beta_u_clipped,
    emv_pp   = ifelse(!is.na(f_pp), eq_ta - ell + f_pp, NA_real_),
    v_pp     = ifelse(!is.na(emv_pp) & !is.na(fu_pp), emv_pp - fu_pp, NA_real_),
    v_no_franchise = eq_ta - ell,
    omo_book = omo_eligible,
    omo_loss = loss_omo_total,
    omo_mv   = pmax(omo_book - omo_loss, 0),
    C   = replace_na(cash, 0),
    D_U = replace_na(uninsured_deposit, 0),
    D   = replace_na(insured_deposit, 0) + D_U,
    avail_liq_omo = C + omo_mv,
    phi   = safe_div(avail_liq_omo, D_U, NA_real_),
    phi_C = safe_div(C, D_U, NA_real_),
    phi_S = safe_div(omo_mv, D_U, NA_real_),
    liq_gap = D_U - avail_liq_omo,
    has_gap = as.integer(liq_gap > 0),
    wholesale_ta = 100 * (fhlb_adv + fed_fund_purchase + repo) / total_asset,
    g_du = safe_div(g_000, D_U, NA_real_),
    # ── Additional variables for descriptive table ──
    ta_billion = total_asset / 1e6,
    htm_ta = 100 * (treasury_htm_amortize + replace_na(state_sec_htm_amortize, 0) +
                    replace_na(agency_rmbs_htm_amortize, 0) +
                    replace_na(other_rmbs_htm_amortize, 0) +
                    replace_na(agency_cmbs_htm_amortize, 0) +
                    replace_na(other_cmbs_htm_amortize, 0) +
                    replace_na(abs_htm_amortize, 0) +
                    replace_na(agency_debt_htm_amortize, 0)) / total_asset,
    afs_ta = 100 * (treasury_hfs_amortize + replace_na(state_sec_hfs_amortize, 0) +
                    replace_na(agency_rmbs_hfs_amortize, 0) +
                    replace_na(other_rmbs_hfs_amortize, 0) +
                    replace_na(agency_cmbs_hfs_amortize, 0) +
                    replace_na(other_cmbs_hfs_amortize, 0) +
                    replace_na(abs_hfs_amortize, 0) +
                    replace_na(agency_debt_hfs_amortize, 0)) / total_asset,
    nonomo_ta = non_omo_eligible_to_total_asset,
    unins_leverage = uninsured_leverage,
    liq_gap_billion = liq_gap / 1e6,
    total_borrowing_million = g / 1e3,
    dw_borrowing_million    = dw_total / 1e3,
    btfp_borrowing_million  = btfp_total / 1e3
  )

cat(sprintf("Crisis-period descriptive panel: %s banks (2022Q4 call report)\n", fmt(nrow(df_desc))))

Crisis-period descriptive panel: 4,696 banks (2022Q4 call report)

cat(sprintf("  Crisis borrowers (Mar 8 – May 4): %s\n", fmt(sum(df_desc$borrowed_crisis))))

Crisis borrowers (Mar 8 – May 4): 860

print(table(df_desc$borrower_type_crisis))

Non-Borrower DW Only BTFP Only Both 3836 341 412 107

# ══════════════════════════════════════════════════════════════════════════
# STEP 2: Define master variable list (Panels A–G, no borrowing vars)
# ══════════════════════════════════════════════════════════════════════════

desc_vars <- tribble(
  ~label,                                  ~col,                  ~digits,
  # ── Panel A: Size & Balance Sheet ──
  "Total Assets ($B)",                     "ta_billion",           2,
  "Log(Total Assets)",                     "log_ta",               2,
  "Book Equity / TA (pp)",                 "eq_ta",                2,
  "Tier 1 Capital / TA (pp)",             "tier1cap_to_total_asset", 2,
  "ROA (pp)",                              "roa",                  3,
  "Cash / TA (pp)",                        "cash_ta",              2,
  "Loan / Deposit",                        "loan_to_deposit",      3,
  "Wholesale / TA (pp)",                   "wholesale_ta",         2,
  "FHLB / TA (pp)",                        "fhlb_ta",              2,
  # ── Panel B: Securities Breakdown ──
  "Total Securities / TA (pp)",            "sec_ta",               2,
  "HTM Securities / TA (pp)",              "htm_ta",               2,
  "AFS Securities / TA (pp)",              "afs_ta",               2,
  "OMO Securities / TA (pp)",              "omo_ta",               2,
  "Non-OMO Securities / TA (pp)",          "nonomo_ta",            2,
  # ── Panel C: Deposits ──
  "Total Deposits / TA (pp)",              "dep_ta",               2,
  "Uninsured Dep / TA (pp)",               "uid_ta",               2,
  "Uninsured / Total Deposits (D^U/D)",    "du_d",                 3,
  "Uninsured Leverage",                    "unins_leverage",       3,
  # ── Panel D: MTM Losses ──
  "Total MTM Loss / TA (pp)",              "ell",                  2,
  "Securities MTM Loss / TA (pp)",         "ell_sec",              2,
  "OMO Sec MTM Loss / TA (pp)",            "ell_omo",              2,
  "Non-OMO Sec MTM Loss / TA (pp)",        "ell_nonomo",           2,
  "Loan MTM Loss / TA (pp)",               "ell_loan",             2,
  # ── Panel E: Deposit Franchise ──
  "Deposit Beta (beta^U)",                 "beta_u_clipped",       3,
  "1 - beta^U",                            "one_minus_beta",       3,
  "Total Franchise F / TA (pp)",           "f_pp",                 2,
  "Uninsured Franchise F^U / TA (pp)",     "fu_pp",                2,
  "Insured Franchise F^I / TA (pp)",       "fi_pp",                2,
  # ── Panel F: Market-Value Solvency ──
  "E^MV / TA (pp)",                        "emv_pp",               2,
  "Run Value v (pp)",                      "v_pp",                 2,
  "E - lambda (no franchise, pp)",         "v_no_franchise",       2,
  # ── Panel G: Liquidity ──
  "phi = (C + S_OMO_MV) / D^U",           "phi",                  3,
  "Cash / D^U",                            "phi_C",                3,
  "S_OMO_MV / D^U",                       "phi_S",                3,
  "Liquidity Gap ($B)",                    "liq_gap_billion",      2,
  "Has Gap (phi < 1) (%)",                 "has_gap",              1
)

# ── Borrowing-only variables (separate panel, no t-test) ──
borrow_vars <- tribble(
  ~label,                                  ~col,                  ~digits,
  "Total Borrowing ($M)",                  "total_borrowing_million", 1,
  "DW Borrowing ($M)",                     "dw_borrowing_million",    1,
  "BTFP Borrowing ($M)",                   "btfp_borrowing_million",  1,
  "Number of DW Loans",                    "dw_n",                    1,
  "Number of BTFP Loans",                  "btfp_n",                  1,
  "Avg DW Rate (%)",                       "dw_mean_rate",            3,
  "Avg BTFP Rate (%)",                     "btfp_mean_rate",          3,
  "Avg DW Term (days)",                    "dw_mean_term",            1,
  "Avg BTFP Term (days)",                  "btfp_mean_term",          1,
  "Avg DW Eff. Maturity (days)",           "dw_mean_effmat",          1,
  "Avg BTFP Eff. Maturity (days)",         "btfp_mean_effmat",        1,
  "Borrowing / D^U",                       "g_du",                    3
)

# ══════════════════════════════════════════════════════════════════════════
# STEP 3: Helper functions
# ══════════════════════════════════════════════════════════════════════════

compute_group_stats <- function(data, var_col, digits = 2) {
  x <- data[[var_col]]
  x <- x[!is.na(x)]
  if (length(x) == 0) return(list(n = 0, mean = NA_real_, sd = NA_real_, median = NA_real_))
  list(n = length(x),
       mean = round(mean(x), digits + 2),
       sd   = round(sd(x), digits + 2),
       median = round(median(x), digits + 2))
}

welch_t_vs_nonborrower <- function(borrow_data, nonborrow_data, var_col) {
  x_b <- borrow_data[[var_col]]; x_b <- x_b[!is.na(x_b)]
  x_n <- nonborrow_data[[var_col]]; x_n <- x_n[!is.na(x_n)]
  if (length(x_b) < 2 || length(x_n) < 2) return(list(t = NA_real_, p = NA_real_, stars = ""))
  result <- tryCatch({
    tt <- t.test(x_b, x_n)
    pv <- tt$p.value; tv <- unname(tt$statistic)
    if (is.na(pv) || is.nan(pv) || is.na(tv) || is.nan(tv)) {
      list(t = NA_real_, p = NA_real_, stars = "")
    } else {
      stars <- if (pv < 0.01) "***" else if (pv < 0.05) "**" else if (pv < 0.10) "*" else ""
      list(t = round(tv, 2), p = pv, stars = stars)
    }
  }, error = function(e) list(t = NA_real_, p = NA_real_, stars = ""),
     warning = function(w) {
       tt <- suppressWarnings(t.test(x_b, x_n))
       pv <- tt$p.value; tv <- unname(tt$statistic)
       if (is.na(pv) || is.nan(pv) || is.na(tv) || is.nan(tv)) {
         list(t = NA_real_, p = NA_real_, stars = "")
       } else {
         stars <- if (pv < 0.01) "***" else if (pv < 0.05) "**" else if (pv < 0.10) "*" else ""
         list(t = round(tv, 2), p = pv, stars = stars)
       }
     })
  result
}

# ══════════════════════════════════════════════════════════════════════════
# STEP 4: Define groups (crisis-period borrower classification)
# ══════════════════════════════════════════════════════════════════════════

grp_nonborrow <- df_desc %>% filter(borrower_type_crisis == "Non-Borrower")
grp_allborrow <- df_desc %>% filter(borrowed_crisis == 1)
grp_btfp      <- df_desc %>% filter(borrower_type_crisis == "BTFP Only")
grp_dw        <- df_desc %>% filter(borrower_type_crisis == "DW Only")
grp_both      <- df_desc %>% filter(borrower_type_crisis == "Both")

# Build the main table row by row (Panels A-G with t-stats)
desc_rows <- map_dfr(1:nrow(desc_vars), function(i) {
  label  <- desc_vars$label[i]
  col    <- desc_vars$col[i]
  dg     <- desc_vars$digits[i]
  
  if (!col %in% names(df_desc)) {
    return(tibble(Variable = label,
      `NB Mean` = NA, `NB SD` = NA, `NB Med` = NA,
      `All Mean` = NA, `All t` = NA,
      `BTFP Mean` = NA, `BTFP t` = NA,
      `DW Mean` = NA, `DW t` = NA,
      `Both Mean` = NA, `Both t` = NA))
  }
  
  nb     <- compute_group_stats(grp_nonborrow, col, dg)
  s_all  <- compute_group_stats(grp_allborrow, col, dg)
  s_bt   <- compute_group_stats(grp_btfp, col, dg)
  s_dw   <- compute_group_stats(grp_dw, col, dg)
  s_bo   <- compute_group_stats(grp_both, col, dg)
  
  t_all  <- welch_t_vs_nonborrower(grp_allborrow, grp_nonborrow, col)
  t_bt   <- welch_t_vs_nonborrower(grp_btfp, grp_nonborrow, col)
  t_dw   <- welch_t_vs_nonborrower(grp_dw, grp_nonborrow, col)
  t_bo   <- welch_t_vs_nonborrower(grp_both, grp_nonborrow, col)
  
  fmt_mean <- function(val, dg) formatC(val, format = "f", digits = dg)
  fmt_t    <- function(tt) if (is.na(tt$t)) "" else paste0("[", formatC(tt$t, format = "f", digits = 2), "]", tt$stars)
  
  tibble(
    Variable   = label,
    `NB Mean`  = fmt_mean(nb$mean, dg),
    `NB SD`    = paste0("(", fmt_mean(nb$sd, dg), ")"),
    `NB Med`   = fmt_mean(nb$median, dg),
    `All Mean` = fmt_mean(s_all$mean, dg),
    `All t`    = fmt_t(t_all),
    `BTFP Mean`= fmt_mean(s_bt$mean, dg),
    `BTFP t`   = fmt_t(t_bt),
    `DW Mean`  = fmt_mean(s_dw$mean, dg),
    `DW t`     = fmt_t(t_dw),
    `Both Mean`= fmt_mean(s_bo$mean, dg),
    `Both t`   = fmt_t(t_bo)
  )
})

# ══════════════════════════════════════════════════════════════════════════
# STEP 5: Add N row + display main table (Panels A-G)
# ══════════════════════════════════════════════════════════════════════════

n_row <- tibble(
  Variable    = "N (banks)",
  `NB Mean`   = fmt(nrow(grp_nonborrow)),
  `NB SD`     = "",
  `NB Med`    = "",
  `All Mean`  = fmt(nrow(grp_allborrow)),
  `All t`     = "",
  `BTFP Mean` = fmt(nrow(grp_btfp)),
  `BTFP t`    = "",
  `DW Mean`   = fmt(nrow(grp_dw)),
  `DW t`      = "",
  `Both Mean` = fmt(nrow(grp_both)),
  `Both t`    = ""
)

desc_final <- bind_rows(n_row, desc_rows)

kbl(desc_final, format = "html", escape = FALSE,
    col.names = c("", "Mean", "SD", "Median",
                  "Mean", "t-stat",
                  "Mean", "t-stat",
                  "Mean", "t-stat",
                  "Mean", "t-stat"),
    caption = sprintf(
      "Descriptive Statistics: Crisis-Period Borrower vs Non-Borrower (2022Q4 Baseline, N = %s)",
      fmt(nrow(df_desc)))) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE, font_size = 10) %>%
  add_header_above(c(" " = 1,
                     "Non-Borrower" = 3,
                     "All Borrowers" = 2,
                     "BTFP Only" = 2,
                     "DW Only" = 2,
                     "Both" = 2)) %>%
  pack_rows("Size & Balance Sheet", 2, 10) %>%
  pack_rows("Securities Breakdown", 11, 15) %>%
  pack_rows("Deposits & Funding", 16, 19) %>%
  pack_rows("MTM Losses (pp of TA)", 20, 24) %>%
  pack_rows("Deposit Franchise", 25, 29) %>%
  pack_rows("Market-Value Solvency", 30, 32) %>%
  pack_rows("Liquidity Coverage", 33, 37) %>%
  footnote(general = "Borrower status defined by DW/BTFP usage during the crisis window
           (March 8 -- May 4, 2023) only. Balance-sheet variables from 2022Q4 call report.
           Welch two-sample t-test vs Non-Borrower. ***p<0.01, **p<0.05, *p<0.10.
           TA = Total Assets. pp = percentage points. OMO = Open Market Operations eligible.
           HTM = Held-to-Maturity. AFS = Available-for-Sale. phi = liquidity coverage ratio.
           F = deposit franchise (DSSW calibration). v = run value = E^MV - F^U.")
Descriptive Statistics: Crisis-Period Borrower vs Non-Borrower (2022Q4 Baseline, N = 4,696)
Non-Borrower
All Borrowers
BTFP Only
DW Only
Both
Mean SD Median Mean t-stat Mean t-stat Mean t-stat Mean t-stat
N (banks) 3,836 860 412 341 107
Size & Balance Sheet
Total Assets (\(B) </td> <td style="text-align:left;"> 1.70 </td> <td style="text-align:left;"> (15.33) </td> <td style="text-align:left;"> 0.26 </td> <td style="text-align:left;"> 5.65 </td> <td style="text-align:left;"> [3.55]*** </td> <td style="text-align:left;"> 4.75 </td> <td style="text-align:left;"> [1.86]* </td> <td style="text-align:left;"> 3.82 </td> <td style="text-align:left;"> [2.78]*** </td> <td style="text-align:left;"> 14.92 </td> <td style="text-align:left;"> [2.36]** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Log(Total Assets) </td> <td style="text-align:left;"> 12.61 </td> <td style="text-align:left;"> (1.40) </td> <td style="text-align:left;"> 12.49 </td> <td style="text-align:left;"> 13.73 </td> <td style="text-align:left;"> [20.12]*** </td> <td style="text-align:left;"> 13.45 </td> <td style="text-align:left;"> [11.57]*** </td> <td style="text-align:left;"> 13.82 </td> <td style="text-align:left;"> [15.12]*** </td> <td style="text-align:left;"> 14.56 </td> <td style="text-align:left;"> [11.24]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Book Equity / TA (pp) </td> <td style="text-align:left;"> 11.38 </td> <td style="text-align:left;"> (11.38) </td> <td style="text-align:left;"> 9.15 </td> <td style="text-align:left;"> 8.59 </td> <td style="text-align:left;"> [-12.99]*** </td> <td style="text-align:left;"> 8.20 </td> <td style="text-align:left;"> [-13.11]*** </td> <td style="text-align:left;"> 9.21 </td> <td style="text-align:left;"> [-8.36]*** </td> <td style="text-align:left;"> 8.11 </td> <td style="text-align:left;"> [-10.11]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Tier 1 Capital / TA (pp) </td> <td style="text-align:left;"> 12.82 </td> <td style="text-align:left;"> (10.62) </td> <td style="text-align:left;"> 10.38 </td> <td style="text-align:left;"> 10.36 </td> <td style="text-align:left;"> [-12.87]*** </td> <td style="text-align:left;"> 10.39 </td> <td style="text-align:left;"> [-11.60]*** </td> <td style="text-align:left;"> 10.55 </td> <td style="text-align:left;"> [-10.01]*** </td> <td style="text-align:left;"> 9.62 </td> <td style="text-align:left;"> [-14.00]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> ROA (pp) </td> <td style="text-align:left;"> 1.419 </td> <td style="text-align:left;"> (12.704) </td> <td style="text-align:left;"> 1.007 </td> <td style="text-align:left;"> 1.109 </td> <td style="text-align:left;"> [-1.50] </td> <td style="text-align:left;"> 1.059 </td> <td style="text-align:left;"> [-1.74]* </td> <td style="text-align:left;"> 1.175 </td> <td style="text-align:left;"> [-1.17] </td> <td style="text-align:left;"> 1.089 </td> <td style="text-align:left;"> [-1.56] </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Cash / TA (pp) </td> <td style="text-align:left;"> 9.55 </td> <td style="text-align:left;"> (11.51) </td> <td style="text-align:left;"> 5.74 </td> <td style="text-align:left;"> 5.39 </td> <td style="text-align:left;"> [-15.14]*** </td> <td style="text-align:left;"> 4.69 </td> <td style="text-align:left;"> [-16.78]*** </td> <td style="text-align:left;"> 6.48 </td> <td style="text-align:left;"> [-6.97]*** </td> <td style="text-align:left;"> 4.61 </td> <td style="text-align:left;"> [-9.42]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Loan / Deposit </td> <td style="text-align:left;"> 71.038 </td> <td style="text-align:left;"> (29.787) </td> <td style="text-align:left;"> 72.156 </td> <td style="text-align:left;"> 74.199 </td> <td style="text-align:left;"> [3.78]*** </td> <td style="text-align:left;"> 72.315 </td> <td style="text-align:left;"> [1.20] </td> <td style="text-align:left;"> 76.597 </td> <td style="text-align:left;"> [4.53]*** </td> <td style="text-align:left;"> 73.808 </td> <td style="text-align:left;"> [1.40] </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Wholesale / TA (pp) </td> <td style="text-align:left;"> 3.14 </td> <td style="text-align:left;"> (4.69) </td> <td style="text-align:left;"> 1.07 </td> <td style="text-align:left;"> 4.82 </td> <td style="text-align:left;"> [8.34]*** </td> <td style="text-align:left;"> 5.07 </td> <td style="text-align:left;"> [7.04]*** </td> <td style="text-align:left;"> 4.34 </td> <td style="text-align:left;"> [4.03]*** </td> <td style="text-align:left;"> 5.32 </td> <td style="text-align:left;"> [3.68]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> FHLB / TA (pp) </td> <td style="text-align:left;"> 2.51 </td> <td style="text-align:left;"> (4.27) </td> <td style="text-align:left;"> 0.00 </td> <td style="text-align:left;"> 3.65 </td> <td style="text-align:left;"> [6.46]*** </td> <td style="text-align:left;"> 3.79 </td> <td style="text-align:left;"> [5.41]*** </td> <td style="text-align:left;"> 3.33 </td> <td style="text-align:left;"> [3.20]*** </td> <td style="text-align:left;"> 4.13 </td> <td style="text-align:left;"> [2.82]*** </td> </tr> <tr grouplength="5"><td colspan="12" style="border-bottom: 1px solid;"><strong>Securities Breakdown</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total Securities / TA (pp) </td> <td style="text-align:left;"> 24.17 </td> <td style="text-align:left;"> (16.72) </td> <td style="text-align:left;"> 21.67 </td> <td style="text-align:left;"> 26.17 </td> <td style="text-align:left;"> [3.56]*** </td> <td style="text-align:left;"> 28.29 </td> <td style="text-align:left;"> [5.53]*** </td> <td style="text-align:left;"> 23.10 </td> <td style="text-align:left;"> [-1.29] </td> <td style="text-align:left;"> 27.78 </td> <td style="text-align:left;"> [2.51]** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> HTM Securities / TA (pp) </td> <td style="text-align:left;"> 3.26 </td> <td style="text-align:left;"> (8.72) </td> <td style="text-align:left;"> 0.00 </td> <td style="text-align:left;"> 3.69 </td> <td style="text-align:left;"> [1.39] </td> <td style="text-align:left;"> 3.30 </td> <td style="text-align:left;"> [0.08] </td> <td style="text-align:left;"> 3.56 </td> <td style="text-align:left;"> [0.68] </td> <td style="text-align:left;"> 5.64 </td> <td style="text-align:left;"> [2.70]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> AFS Securities / TA (pp) </td> <td style="text-align:left;"> 18.98 </td> <td style="text-align:left;"> (16.24) </td> <td style="text-align:left;"> 15.98 </td> <td style="text-align:left;"> 20.66 </td> <td style="text-align:left;"> [3.04]*** </td> <td style="text-align:left;"> 22.51 </td> <td style="text-align:left;"> [4.69]*** </td> <td style="text-align:left;"> 18.56 </td> <td style="text-align:left;"> [-0.51] </td> <td style="text-align:left;"> 20.25 </td> <td style="text-align:left;"> [0.99] </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> OMO Securities / TA (pp) </td> <td style="text-align:left;"> 11.48 </td> <td style="text-align:left;"> (11.24) </td> <td style="text-align:left;"> 8.66 </td> <td style="text-align:left;"> 12.93 </td> <td style="text-align:left;"> [3.62]*** </td> <td style="text-align:left;"> 13.95 </td> <td style="text-align:left;"> [4.37]*** </td> <td style="text-align:left;"> 11.15 </td> <td style="text-align:left;"> [-0.59] </td> <td style="text-align:left;"> 14.69 </td> <td style="text-align:left;"> [3.08]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Non-OMO Securities / TA (pp) </td> <td style="text-align:left;"> 72.49 </td> <td style="text-align:left;"> (16.83) </td> <td style="text-align:left;"> 77.00 </td> <td style="text-align:left;"> 75.87 </td> <td style="text-align:left;"> [6.83]*** </td> <td style="text-align:left;"> 75.69 </td> <td style="text-align:left;"> [4.93]*** </td> <td style="text-align:left;"> 76.38 </td> <td style="text-align:left;"> [5.34]*** </td> <td style="text-align:left;"> 74.93 </td> <td style="text-align:left;"> [2.11]** </td> </tr> <tr grouplength="4"><td colspan="12" style="border-bottom: 1px solid;"><strong>Deposits &amp; Funding</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total Deposits / TA (pp) </td> <td style="text-align:left;"> 84.22 </td> <td style="text-align:left;"> (13.79) </td> <td style="text-align:left;"> 87.39 </td> <td style="text-align:left;"> 85.47 </td> <td style="text-align:left;"> [3.91]*** </td> <td style="text-align:left;"> 85.84 </td> <td style="text-align:left;"> [4.12]*** </td> <td style="text-align:left;"> 85.22 </td> <td style="text-align:left;"> [2.38]** </td> <td style="text-align:left;"> 84.82 </td> <td style="text-align:left;"> [0.80] </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured Dep / TA (pp) </td> <td style="text-align:left;"> 22.40 </td> <td style="text-align:left;"> (12.28) </td> <td style="text-align:left;"> 21.02 </td> <td style="text-align:left;"> 27.11 </td> <td style="text-align:left;"> [10.30]*** </td> <td style="text-align:left;"> 26.07 </td> <td style="text-align:left;"> [6.16]*** </td> <td style="text-align:left;"> 26.88 </td> <td style="text-align:left;"> [6.60]*** </td> <td style="text-align:left;"> 31.83 </td> <td style="text-align:left;"> [7.00]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured / Total Deposits (D^U/D) </td> <td style="text-align:left;"> 0.268 </td> <td style="text-align:left;"> (0.144) </td> <td style="text-align:left;"> 0.247 </td> <td style="text-align:left;"> 0.316 </td> <td style="text-align:left;"> [9.15]*** </td> <td style="text-align:left;"> 0.303 </td> <td style="text-align:left;"> [5.14]*** </td> <td style="text-align:left;"> 0.315 </td> <td style="text-align:left;"> [5.95]*** </td> <td style="text-align:left;"> 0.373 </td> <td style="text-align:left;"> [6.67]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured Leverage </td> <td style="text-align:left;"> 22.404 </td> <td style="text-align:left;"> (12.281) </td> <td style="text-align:left;"> 21.024 </td> <td style="text-align:left;"> 27.110 </td> <td style="text-align:left;"> [10.30]*** </td> <td style="text-align:left;"> 26.072 </td> <td style="text-align:left;"> [6.16]*** </td> <td style="text-align:left;"> 26.881 </td> <td style="text-align:left;"> [6.60]*** </td> <td style="text-align:left;"> 31.835 </td> <td style="text-align:left;"> [7.00]*** </td> </tr> <tr grouplength="5"><td colspan="12" style="border-bottom: 1px solid;"><strong>MTM Losses (pp of TA)</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total MTM Loss / TA (pp) </td> <td style="text-align:left;"> 8.40 </td> <td style="text-align:left;"> (7.43) </td> <td style="text-align:left;"> 7.78 </td> <td style="text-align:left;"> 9.28 </td> <td style="text-align:left;"> [5.07]*** </td> <td style="text-align:left;"> 9.63 </td> <td style="text-align:left;"> [5.68]*** </td> <td style="text-align:left;"> 8.81 </td> <td style="text-align:left;"> [1.79]* </td> <td style="text-align:left;"> 9.42 </td> <td style="text-align:left;"> [2.70]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Securities MTM Loss / TA (pp) </td> <td style="text-align:left;"> 3.52 </td> <td style="text-align:left;"> (3.78) </td> <td style="text-align:left;"> 2.69 </td> <td style="text-align:left;"> 4.21 </td> <td style="text-align:left;"> [5.81]*** </td> <td style="text-align:left;"> 4.56 </td> <td style="text-align:left;"> [6.46]*** </td> <td style="text-align:left;"> 3.63 </td> <td style="text-align:left;"> [0.69] </td> <td style="text-align:left;"> 4.68 </td> <td style="text-align:left;"> [3.70]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> OMO Sec MTM Loss / TA (pp) </td> <td style="text-align:left;"> 1.03 </td> <td style="text-align:left;"> (1.44) </td> <td style="text-align:left;"> 0.56 </td> <td style="text-align:left;"> 1.59 </td> <td style="text-align:left;"> [8.43]*** </td> <td style="text-align:left;"> 1.60 </td> <td style="text-align:left;"> [5.84]*** </td> <td style="text-align:left;"> 1.42 </td> <td style="text-align:left;"> [4.27]*** </td> <td style="text-align:left;"> 2.11 </td> <td style="text-align:left;"> [5.62]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Non-OMO Sec MTM Loss / TA (pp) </td> <td style="text-align:left;"> 2.48 </td> <td style="text-align:left;"> (3.45) </td> <td style="text-align:left;"> 1.53 </td> <td style="text-align:left;"> 2.62 </td> <td style="text-align:left;"> [1.26] </td> <td style="text-align:left;"> 2.97 </td> <td style="text-align:left;"> [3.29]*** </td> <td style="text-align:left;"> 2.21 </td> <td style="text-align:left;"> [-1.98]** </td> <td style="text-align:left;"> 2.57 </td> <td style="text-align:left;"> [0.30] </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Loan MTM Loss / TA (pp) </td> <td style="text-align:left;"> 4.89 </td> <td style="text-align:left;"> (5.25) </td> <td style="text-align:left;"> 3.99 </td> <td style="text-align:left;"> 5.07 </td> <td style="text-align:left;"> [1.48] </td> <td style="text-align:left;"> 5.07 </td> <td style="text-align:left;"> [1.13] </td> <td style="text-align:left;"> 5.18 </td> <td style="text-align:left;"> [1.73]* </td> <td style="text-align:left;"> 4.74 </td> <td style="text-align:left;"> [-0.58] </td> </tr> <tr grouplength="5"><td colspan="12" style="border-bottom: 1px solid;"><strong>Deposit Franchise</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Deposit Beta (beta^U) </td> <td style="text-align:left;"> 0.331 </td> <td style="text-align:left;"> (0.105) </td> <td style="text-align:left;"> 0.310 </td> <td style="text-align:left;"> 0.343 </td> <td style="text-align:left;"> [2.75]*** </td> <td style="text-align:left;"> 0.337 </td> <td style="text-align:left;"> [0.97] </td> <td style="text-align:left;"> 0.344 </td> <td style="text-align:left;"> [1.96]* </td> <td style="text-align:left;"> 0.365 </td> <td style="text-align:left;"> [2.37]** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> 1 - beta^U </td> <td style="text-align:left;"> 0.669 </td> <td style="text-align:left;"> (0.105) </td> <td style="text-align:left;"> 0.690 </td> <td style="text-align:left;"> 0.657 </td> <td style="text-align:left;"> [-2.75]*** </td> <td style="text-align:left;"> 0.663 </td> <td style="text-align:left;"> [-0.97] </td> <td style="text-align:left;"> 0.656 </td> <td style="text-align:left;"> [-1.96]* </td> <td style="text-align:left;"> 0.635 </td> <td style="text-align:left;"> [-2.37]** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total Franchise F / TA (pp) </td> <td style="text-align:left;"> 2.33 </td> <td style="text-align:left;"> (1.50) </td> <td style="text-align:left;"> 2.07 </td> <td style="text-align:left;"> 2.71 </td> <td style="text-align:left;"> [6.36]*** </td> <td style="text-align:left;"> 2.66 </td> <td style="text-align:left;"> [4.16]*** </td> <td style="text-align:left;"> 2.68 </td> <td style="text-align:left;"> [3.93]*** </td> <td style="text-align:left;"> 2.97 </td> <td style="text-align:left;"> [3.79]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured Franchise F^U / TA (pp) </td> <td style="text-align:left;"> 0.79 </td> <td style="text-align:left;"> (0.99) </td> <td style="text-align:left;"> 0.51 </td> <td style="text-align:left;"> 1.02 </td> <td style="text-align:left;"> [5.82]*** </td> <td style="text-align:left;"> 0.96 </td> <td style="text-align:left;"> [3.30]*** </td> <td style="text-align:left;"> 1.00 </td> <td style="text-align:left;"> [3.66]*** </td> <td style="text-align:left;"> 1.28 </td> <td style="text-align:left;"> [4.16]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Insured Franchise F^I / TA (pp) </td> <td style="text-align:left;"> 1.54 </td> <td style="text-align:left;"> (0.70) </td> <td style="text-align:left;"> 1.54 </td> <td style="text-align:left;"> 1.69 </td> <td style="text-align:left;"> [5.64]*** </td> <td style="text-align:left;"> 1.70 </td> <td style="text-align:left;"> [4.51]*** </td> <td style="text-align:left;"> 1.68 </td> <td style="text-align:left;"> [3.49]*** </td> <td style="text-align:left;"> 1.69 </td> <td style="text-align:left;"> [1.94]* </td> </tr> <tr grouplength="3"><td colspan="12" style="border-bottom: 1px solid;"><strong>Market-Value Solvency</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> E^MV / TA (pp) </td> <td style="text-align:left;"> 4.26 </td> <td style="text-align:left;"> (8.79) </td> <td style="text-align:left;"> 3.66 </td> <td style="text-align:left;"> 2.09 </td> <td style="text-align:left;"> [-8.98]*** </td> <td style="text-align:left;"> 1.28 </td> <td style="text-align:left;"> [-9.69]*** </td> <td style="text-align:left;"> 3.11 </td> <td style="text-align:left;"> [-3.30]*** </td> <td style="text-align:left;"> 1.92 </td> <td style="text-align:left;"> [-4.26]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Run Value v (pp) </td> <td style="text-align:left;"> 3.47 </td> <td style="text-align:left;"> (8.66) </td> <td style="text-align:left;"> 2.98 </td> <td style="text-align:left;"> 1.07 </td> <td style="text-align:left;"> [-10.26]*** </td> <td style="text-align:left;"> 0.32 </td> <td style="text-align:left;"> [-10.66]*** </td> <td style="text-align:left;"> 2.11 </td> <td style="text-align:left;"> [-4.06]*** </td> <td style="text-align:left;"> 0.64 </td> <td style="text-align:left;"> [-5.37]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> E - lambda (no franchise, pp) </td> <td style="text-align:left;"> 2.77 </td> <td style="text-align:left;"> (13.78) </td> <td style="text-align:left;"> 1.52 </td> <td style="text-align:left;"> -0.69 </td> <td style="text-align:left;"> [-11.81]*** </td> <td style="text-align:left;"> -1.43 </td> <td style="text-align:left;"> [-12.18]*** </td> <td style="text-align:left;"> 0.40 </td> <td style="text-align:left;"> [-6.24]*** </td> <td style="text-align:left;"> -1.31 </td> <td style="text-align:left;"> [-6.97]*** </td> </tr> <tr grouplength="5"><td colspan="12" style="border-bottom: 1px solid;"><strong>Liquidity Coverage</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> phi = (C + S_OMO_MV) / D^U </td> <td style="text-align:left;"> 2.903 </td> <td style="text-align:left;"> (49.809) </td> <td style="text-align:left;"> 0.741 </td> <td style="text-align:left;"> 0.938 </td> <td style="text-align:left;"> [-2.37]** </td> <td style="text-align:left;"> 0.806 </td> <td style="text-align:left;"> [-2.58]*** </td> <td style="text-align:left;"> 1.171 </td> <td style="text-align:left;"> [-1.91]* </td> <td style="text-align:left;"> 0.698 </td> <td style="text-align:left;"> [-2.70]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Cash / D^U </td> <td style="text-align:left;"> 1.979 </td> <td style="text-align:left;"> (44.992) </td> <td style="text-align:left;"> 0.282 </td> <td style="text-align:left;"> 0.375 </td> <td style="text-align:left;"> [-2.16]** </td> <td style="text-align:left;"> 0.224 </td> <td style="text-align:left;"> [-2.40]** </td> <td style="text-align:left;"> 0.624 </td> <td style="text-align:left;"> [-1.70]* </td> <td style="text-align:left;"> 0.160 </td> <td style="text-align:left;"> [-2.48]** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> S_OMO_MV / D^U </td> <td style="text-align:left;"> 0.925 </td> <td style="text-align:left;"> (7.477) </td> <td style="text-align:left;"> 0.369 </td> <td style="text-align:left;"> 0.563 </td> <td style="text-align:left;"> [-2.80]*** </td> <td style="text-align:left;"> 0.583 </td> <td style="text-align:left;"> [-2.69]*** </td> <td style="text-align:left;"> 0.548 </td> <td style="text-align:left;"> [-2.44]** </td> <td style="text-align:left;"> 0.538 </td> <td style="text-align:left;"> [-2.59]*** </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Liquidity Gap (\)B) 0.15 (2.74) 0.01 0.98 [4.10]*** 0.78 [2.12]** 0.68 [2.11]** 2.65 [3.36]***
Has Gap (phi < 1) (%) 0.6 (0.5) 1.0 0.8 [10.22]*** 0.8 [7.98]*** 0.8 [5.57]*** 0.8 [6.18]***
Note:
Borrower status defined by DW/BTFP usage during the crisis window
(March 8 – May 4, 2023) only. Balance-sheet variables from 2022Q4 call report.
Welch two-sample t-test vs Non-Borrower. p<0.01, p<0.05, p<0.10.
TA = Total Assets. pp = percentage points. OMO = Open Market Operations eligible.
HTM = Held-to-Maturity. AFS = Available-for-Sale. phi = liquidity coverage ratio.
F = deposit franchise (DSSW calibration). v = run value = E^MV - F^U.
# ══════════════════════════════════════════════════════════════════════════
# STEP 6: Borrowing Characteristics panel (borrowers only, no t-test)
# ══════════════════════════════════════════════════════════════════════════

fmt_mean_b <- function(val, dg) {
  if (is.na(val)) return("\u2014")
  formatC(val, format = "f", digits = dg)
}

borrow_rows <- map_dfr(1:nrow(borrow_vars), function(i) {
  label <- borrow_vars$label[i]
  col   <- borrow_vars$col[i]
  dg    <- borrow_vars$digits[i]
  
  if (!col %in% names(df_desc)) {
    return(tibble(Variable = label,
      `All Borrowers` = NA, `BTFP Only` = NA,
      `DW Only` = NA, `Both` = NA))
  }
  
  s_all  <- compute_group_stats(grp_allborrow, col, dg)
  s_btfp <- compute_group_stats(grp_btfp, col, dg)
  s_dw   <- compute_group_stats(grp_dw, col, dg)
  s_both <- compute_group_stats(grp_both, col, dg)
  
  tibble(
    Variable         = label,
    `All Borrowers`  = fmt_mean_b(s_all$mean, dg),
    `BTFP Only`      = fmt_mean_b(s_btfp$mean, dg),
    `DW Only`        = fmt_mean_b(s_dw$mean, dg),
    `Both`           = fmt_mean_b(s_both$mean, dg)
  )
})

borrow_n_row <- tibble(
  Variable         = "N (banks)",
  `All Borrowers`  = fmt(nrow(grp_allborrow)),
  `BTFP Only`      = fmt(nrow(grp_btfp)),
  `DW Only`        = fmt(nrow(grp_dw)),
  `Both`           = fmt(nrow(grp_both))
)

borrow_final <- bind_rows(borrow_n_row, borrow_rows)

cat("\n\n### Panel H: Borrowing Characteristics (Crisis Borrowers Only, Mar 8 -- May 4)\n\n")

11.0.1 Panel H: Borrowing Characteristics (Crisis Borrowers Only, Mar 8 – May 4)

kbl(borrow_final, format = "html", escape = FALSE,
    col.names = c("", "All Borrowers", "BTFP Only", "DW Only", "Both"),
    caption = "Panel H: Borrowing Characteristics -- Crisis Period (Mar 8 -- May 4, 2023)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 10) %>%
  pack_rows("Amounts", 2, 4) %>%
  pack_rows("Loan Counts", 5, 6) %>%
  pack_rows("Rates", 7, 8) %>%
  pack_rows("Terms & Maturity", 9, 12) %>%
  pack_rows("Intensity", 13, 13) %>%
  footnote(general = "Bank-level means across all loans taken during the crisis window
           (March 8 -- May 4, 2023). Rate, term, and effective maturity are averages of
           loan-level values. Borrowing/D^U = total crisis borrowing / uninsured
           deposits. '\\u2014' = no observations for that facility type.")
Panel H: Borrowing Characteristics – Crisis Period (Mar 8 – May 4, 2023)
All Borrowers BTFP Only DW Only Both
N (banks) 860 412 341 107
Amounts
Total Borrowing (\(M) </td> <td style="text-align:left;"> 778167.7 </td> <td style="text-align:left;"> 204844.7 </td> <td style="text-align:left;"> 180127.2 </td> <td style="text-align:left;"> 4891634.2 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> DW Borrowing (\)M) 625737.6 0.0 180127.2 4455242.9
BTFP Borrowing ($M) 152430.1 204844.7 0.0 436391.3
Loan Counts
Number of DW Loans 2.1 0.0 3.7 5.1
Number of BTFP Loans 1.5 2.4 0.0 2.9
Rates
Avg DW Rate (%) 4.911 4.913 4.908
Avg BTFP Rate (%) 4.700 4.694 4.721
Terms & Maturity
Avg DW Term (days) 5.4 4.4 8.7
Avg BTFP Term (days) 311.3 326.6 252.5
Avg DW Eff. Maturity (days) 3.2 2.8 4.3
Avg BTFP Eff. Maturity (days) 184.1 196.6 136.1
Intensity
Borrowing / D^U 0.391 0.367 0.320 0.708
Note:
Bank-level means across all loans taken during the crisis window
(March 8 – May 4, 2023). Rate, term, and effective maturity are averages of
loan-level values. Borrowing/D^U = total crisis borrowing / uninsured
deposits. ‘014’ = no observations for that facility type.
# ══════════════════════════════════════════════════════════════════════════
# STEP 7: Save both tables as LaTeX
# ══════════════════════════════════════════════════════════════════════════

save_kbl_latex_fn <- function(df_in, filename, col.names, caption) {
  tex <- kbl(df_in, format = "latex", booktabs = TRUE, escape = FALSE,
    col.names = col.names, caption = caption) %>%
    kable_styling(latex_options = c("hold_position", "scale_down"))
  writeLines(tex, file.path(TABLE_PATH, paste0(filename, ".tex")))
  cat(sprintf("Saved: %s.tex\n", filename))
}

save_kbl_latex_fn(desc_final, "Table_DiffMeans_Borrower_vs_NonBorrower_Crisis",
  col.names = c("", "Mean", "SD", "Median",
                "Mean", "t-stat", "Mean", "t-stat",
                "Mean", "t-stat", "Mean", "t-stat"),
  caption = "Descriptive Statistics: Crisis-Period Borrower vs Non-Borrower (2022Q4 Baseline)")

Saved: Table_DiffMeans_Borrower_vs_NonBorrower_Crisis.tex

save_kbl_latex_fn(borrow_final, "Table_BorrowingCharacteristics_Crisis",
  col.names = c("", "All Borrowers", "BTFP Only", "DW Only", "Both"),
  caption = "Borrowing Characteristics -- Crisis Period (Mar 8 -- May 4, 2023)")

Saved: Table_BorrowingCharacteristics_Crisis.tex

cat("\n\n**Both tables saved to LaTeX.**\n")

Both tables saved to LaTeX.


12 KEY PLOTS

# ── Cross-partial heatmap ──
df_heat <- df %>%
  filter(!is.na(ell), !is.na(du_d)) %>%
  mutate(ell_q = ntile(ell, 5), mu_q = ntile(du_d, 5)) %>%
  group_by(ell_q, mu_q) %>%
  summarise(pr_borrow = mean(borrowed), N = n(), .groups = "drop")

p_heat <- ggplot(df_heat, aes(x = factor(mu_q), y = factor(ell_q), fill = pr_borrow)) +
  geom_tile(color = "white", linewidth = 0.5) +
  geom_text(aes(label = sprintf("%.1f%%\n(N=%d)", 100*pr_borrow, N)), color = "white", size = 3) +
  scale_fill_gradient2(low = "#1565C0", mid = "#FFC107", high = "#E53935",
                       midpoint = median(df_heat$pr_borrow), labels = percent) +
  labs(title = "Pr(Borrow) by MTM Loss × Uninsured Deposit Share",
       subtitle = "Cross-partial (Eq. 13): loss effect steepens as mu rises",
       x = "mu = D^U/D Quintile (1=Low, 5=High)",
       y = "MTM Loss ell Quintile (1=Low, 5=High)", fill = "Pr(Borrow)") +
  theme_gp + theme(panel.grid = element_blank())

# ── Dual channel ──
p_ch1 <- ggplot(df %>% filter(!is.na(v_pp)),
       aes(x = ell, y = v_pp, color = factor(borrowed))) +
  geom_point(alpha = 0.2, size = 1) + geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  geom_smooth(method = "lm", se = TRUE, linewidth = 0.8) +
  scale_color_manual(values = c("0" = "grey60", "1" = "#E53935"), labels = c("Non-Borrower","Borrower")) +
  labs(title = "Solvency Erosion", x = "MTM Loss / TA (pp)", y = "Run Value v (pp)", color = "") + theme_gp

p_ch2 <- ggplot(df %>% filter(!is.na(phi)),
       aes(x = ell_omo, y = pmin(phi, 5), color = factor(borrowed))) +
  geom_point(alpha = 0.2, size = 1) + geom_hline(yintercept = 1, linetype = "dashed", color = "red") +
  geom_smooth(method = "lm", se = TRUE, linewidth = 0.8) +
  scale_color_manual(values = c("0" = "grey60", "1" = "#E53935"), labels = c("Non-Borrower","Borrower")) +
  labs(title = "Liquidity Diminishment", x = "OMO Sec MTM / TA (pp)", y = "phi", color = "") + theme_gp

print(p_heat)

print(p_ch1 | p_ch2)


cat(sprintf("\nFinal crisis panel:    %s banks\n", fmt(nrow(df))))
## 
## Final crisis panel:    4,700 banks
cat(sprintf("Final arb panel:       %s banks\n", fmt(nrow(df_arb))))
## Final arb panel:       4,604 banks
cat(sprintf("Final regression N:    %s banks\n", fmt(nrow(df_reg))))
## Final regression N:    4,626 banks
cat(sprintf("  Borrowers:           %s\n", fmt(sum(df_reg$borrowed))))
##   Borrowers:           2,164
cat(sprintf("  Non-borrowers:       %s\n", fmt(sum(1-df_reg$borrowed))))
##   Non-borrowers:       2,462