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