1. Setup

1.1 Libraries

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

1.2 Helpers

# ── Basic utilities ────────────────────────────────────────────────────
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])
}

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
}

fmt  <- function(x, d = 0) formatC(x, format = "f", digits = d, big.mark = ",")
fmt2 <- function(x) formatC(x, format = "f", digits = 2, big.mark = ",")
pct  <- function(x) sprintf("%.1f%%", 100 * mean(x, na.rm = TRUE))

stars_pval <- function(p) dplyr::case_when(
  is.na(p) ~ "", p < 0.01 ~ "***", p < 0.05 ~ "**", p < 0.10 ~ "*", TRUE ~ ""
)

fmt_est <- function(est, p) if (is.na(est)) "" else sprintf("%.3f%s", est, stars_pval(p))
fmt_se  <- function(se)     if (is.na(se))  "" else sprintf("(%.3f)", se)

# ── Winsorize + z-standardize a set of columns in one step ─────────────
wz_cols <- function(data, vars) {
  vars <- vars[vars %in% names(data)]
  data %>%
    mutate(across(all_of(vars), ~ winsorize(.x), .names = "{.col}_w")) %>%
    mutate(across(all_of(paste0(vars, "_w")), ~ z_std(.x), .names = "{.col}_z"))
}

1.3 Paths and file-saving helpers

BASE_PATH  <- "C:/Users/mferdo2/OneDrive - Louisiana State University/Finance_PhD/Research 2026"
DATA_DIR   <- file.path(BASE_PATH, "01_data/Final dataset")
OUT_PATH   <- file.path(BASE_PATH, "03_documentation/results_02")
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)

save_kbl_latex <- function(df, filename, col.names = NULL, caption = "",
                           align = NULL, ...) {
  tex <- kbl(df, format = "latex", booktabs = TRUE, escape = FALSE,
             col.names = col.names, caption = caption, align = align, ...) %>%
    kable_styling(latex_options = c("hold_position"))
  writeLines(tex, file.path(TABLE_PATH, paste0(filename, ".tex")))
  invisible()
}

save_figure <- function(plot_obj, filename, width = 12, height = 8) {
  ggsave(file.path(FIG_PATH, paste0(filename, ".pdf")),
         plot = plot_obj, width = width, height = height, device = "pdf")
  invisible()
}

1.4 Calibration, periods, and plotting theme

# ── Theory constants ───────────────────────────────────────────────────
y_10yr            <- 0.0392
delta_decay       <- 0.10
cap_factor        <- 1 / (y_10yr + delta_decay)
phi_star_baseline <- 1.0
phi_star_struct   <- 0.5

# ── Period dates ───────────────────────────────────────────────────────
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")

assign_period <- function(d) dplyr::case_when(
  d >= P0_start  & d <= P0_end   ~ "P0: Pre-Crisis",
  d >= P1_start  & d <= P1_end   ~ "P1: SVB Week",
  d >= P2_start  & d <= P2_end   ~ "P2: SVB to FRC",
  d >= P3_start  & d <= P3_end   ~ "P3: FRC Week",
  d >= ARB_START & d <= ARB_END  ~ "Arbitrage",
  d >  CRISIS_END & d < ARB_START  ~ "Inter-Period",
  d >  ARB_END   & d <= BTFP_CLOSE ~ "Post-Arbitrage",
  TRUE ~ "Other"
)

period_order <- c("P0: Pre-Crisis", "P1: SVB Week",
                  "P2: SVB to FRC", "P3: FRC Week")

col_specs <- tibble(
  col_id   = c("P0: Pre-Crisis | DW", "P1: SVB Week | DW",
               "P2: SVB to FRC | DW", "P2: SVB to FRC | BTFP",
               "P3: FRC Week | DW",   "P3: FRC Week | BTFP"),
  period   = c("P0: Pre-Crisis", "P1: SVB Week",
               "P2: SVB to FRC", "P2: SVB to FRC",
               "P3: FRC Week",   "P3: FRC Week"),
  facility = c("DW", "DW", "DW", "BTFP", "DW", "BTFP")
)

# ── Plotting ───────────────────────────────────────────────────────────
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.5 Flexible model wrapper (LPM / logit / probit switchable)

# ── GLOBAL MODEL FAMILY ────────────────────────────────────────────────
# Change this single value to switch all extensive-margin regressions
# between: "lpm", "logit", "probit"
# ────────────────────────────────────────────────────────────────────────
MODEL_FAMILY <- "lpm"

#' Fit model with family dispatch
#' @param formula A formula
#' @param data A data frame
#' @param family One of "lpm", "logit", "probit"
fit_model <- function(formula, data, family = MODEL_FAMILY) {
  switch(
    family,
    "lpm"    = lm(formula, data = data),
    "logit"  = glm(formula, data = data, family = binomial(link = "logit")),
    "probit" = glm(formula, data = data, family = binomial(link = "probit")),
    stop(sprintf("Unknown model family: '%s'", family))
  )
}

#' Robust coefficient tidy (works for lm and glm)
#' Uses HC1 for lm, HC0 (sandwich) for glm.
get_robust_tidy <- function(model, vcov_type = "HC1") {
  V <- if (inherits(model, "glm") && !inherits(model, "lm")) {
    sandwich::vcovHC(model, type = "HC0")
  } else {
    sandwich::vcovHC(model, type = vcov_type)
  }
  ct <- lmtest::coeftest(model, vcov. = V)
  tibble::tibble(
    term      = rownames(ct),
    estimate  = ct[, 1],
    std.error = ct[, 2],
    p.value   = ct[, 4]
  )
}

#' Extract model fit stats (works for lm and glm)
get_fit_stats <- function(model) {
  n <- nobs(model)
  if (inherits(model, "glm") && !inherits(model, "lm")) {
    # Pseudo-R² (McFadden)
    ll0 <- logLik(update(model, . ~ 1))
    pr2 <- 1 - as.numeric(logLik(model) / ll0)
    list(n = n, fit_label = "Pseudo R²", fit_value = pr2)
  } else {
    list(n = n, fit_label = "R²", fit_value = summary(model)$r.squared)
  }
}

#' Joint equality test across two subsamples (Chow test)
chow_test <- function(rhs, dv, data, split_var) {
  rest_f <- as.formula(paste(dv, "~", split_var, "+", rhs))
  full_f <- as.formula(paste(dv, "~", split_var, "*(", rhs, ")"))
  m_rest <- fit_model(rest_f, data)
  m_full <- fit_model(full_f, data)
  V <- if (inherits(m_full, "glm") && !inherits(m_full, "lm"))
         sandwich::vcovHC(m_full, type = "HC0")
       else sandwich::vcovHC(m_full, type = "HC1")
  w <- lmtest::waldtest(m_rest, m_full, vcov = V)
  tibble(chi2 = w$F[2], p_value = w$`Pr(>F)`[2])
}

cat(sprintf("Model family in use: %s\n", toupper(MODEL_FAMILY)))
## Model family in use: LPM

2. Data Loading and Variable Construction

2.1 Load raw datasets

call_raw <- read_csv(file.path(DATA_DIR, "final_call_gsib.csv"),
                     show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))

btfp_raw <- read_csv(file.path(DATA_DIR, "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_DIR, "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_maturity_date     = ymd(dw_maturity_date),
         dw_credit_type_clean = str_squish(as.character(dw_credit_type))) %>%
  filter(str_detect(dw_credit_type_clean, "^Primary Credit"))

dssw_raw <- read_csv(file.path(DATA_DIR, "dssw_deposit_betas.csv"),
                     show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))

dssw_costs_file <- file.path(DATA_DIR, "dssw_deposit_costs.csv")
HAS_DEPOSIT_COSTS <- file.exists(dssw_costs_file)
if (HAS_DEPOSIT_COSTS) {
  dssw_costs_raw <- read_csv(dssw_costs_file, show_col_types = FALSE) %>%
    mutate(idrssd = as.character(idrssd))
}

cat(sprintf("Call panel : %s bank-quarters, %s banks\n",
            fmt(nrow(call_raw)), fmt(n_distinct(call_raw$idrssd))))
## Call panel : 75,989 bank-quarters, 5,074 banks
cat(sprintf("BTFP loans : %s transactions, %s banks\n",
            fmt(nrow(btfp_raw)), fmt(n_distinct(btfp_raw$rssd_id))))
## BTFP loans : 6,734 transactions, 1,327 banks
cat(sprintf("DW loans   : %s transactions, %s banks\n",
            fmt(nrow(dw_raw)), fmt(n_distinct(dw_raw$rssd_id))))
## DW loans   : 9,673 transactions, 1,468 banks
cat(sprintf("DSSW betas : %s bank-quarters, %s banks\n",
            fmt(nrow(dssw_raw)), fmt(n_distinct(dssw_raw$idrssd))))
## DSSW betas : 13,588 bank-quarters, 4,660 banks

2.2 DSSW merge + franchise variables

dssw_for_quarter <- function(q) {
  dssw_raw %>%
    filter(estimation_date == q) %>%
    select(idrssd, beta_overall, beta_uninsured, beta_insured,
           beta_uninsured_w, beta_insured_w)
}

dssw_costs_for_quarter <- function(q) {
  if (!HAS_DEPOSIT_COSTS)
    return(tibble(idrssd = character(),
                  deposit_cost_insured = numeric(),
                  deposit_cost_uninsured = numeric(),
                  deposit_cost_weighted = numeric()))
  dssw_costs_raw %>%
    filter(period == q) %>%
    select(idrssd, deposit_cost_insured,
           deposit_cost_uninsured, deposit_cost_weighted)
}

add_franchise_vars <- function(data) {
  data %>%
    mutate(
      beta_clipped   = pmax(0, pmin(1, beta_overall)),
      beta_u_clipped = pmax(0, pmin(1, beta_uninsured_w)),
      beta_i_clipped = pmax(0, pmin(1, beta_insured_w)),
      cost_u_raw = ifelse(HAS_DEPOSIT_COSTS & !is.na(deposit_cost_uninsured),
                          deposit_cost_uninsured, 0),
      cost_i_raw = ifelse(HAS_DEPOSIT_COSTS & !is.na(deposit_cost_insured),
                          deposit_cost_insured, 0),
      d_u_over_ta       = safe_div(uninsured_deposit, total_asset, 0),
      d_i_over_ta       = safe_div(pmax(total_deposit - uninsured_deposit, 0),
                                   total_asset, 0),
      d_over_ta         = safe_div(total_deposit, total_asset, 0),
      uninsured_share_d = safe_div(uninsured_deposit, total_deposit, 0),
      insured_share_d   = pmax(1 - uninsured_share_d, 0),
      rent_u = pmax((1 - beta_u_clipped) * y_10yr - cost_u_raw, 0),
      rent_i = pmax((1 - beta_i_clipped) * y_10yr - cost_i_raw, 0),
      fu_ta = ifelse(!is.na(beta_u_clipped),
                     rent_u * cap_factor * d_u_over_ta * 100, NA_real_),
      fi_ta = ifelse(!is.na(beta_i_clipped),
                     rent_i * cap_factor * d_i_over_ta * 100, NA_real_),
      f_ta  = rowSums(cbind(fu_ta, fi_ta), na.rm = FALSE),
      F_total     = f_ta  / 100 * total_asset,
      F_uninsured = fu_ta / 100 * total_asset,
      F_insured   = fi_ta / 100 * total_asset
    )
}

2.3 Build 2022Q4 cross-section with all derived variables

df <- call_raw %>%
  filter(period == "2022Q4", gsib == 0, failed_bank == 0) %>%
  left_join(dssw_for_quarter("2022Q4"),       by = "idrssd") %>%
  left_join(dssw_costs_for_quarter("2022Q4"), by = "idrssd") %>%
  add_franchise_vars() %>%
  mutate(
    log_ta  = log(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,
    loan_ta = total_loan_to_total_asset,
    dep_ta  = total_deposit_to_total_asset,
    du_ta   = uninsured_leverage,
    mu      = safe_div(uninsured_deposit, total_deposit, 0),
    ell        = mtm_loss_to_total_asset,
    ell_sec    = loss_total_sec_to_ta,
    ell_omo    = loss_omo_sec_to_ta,
    ell_nonomo = loss_nonomo_sec_to_ta,
    ell_loan   = loss_total_loan_to_ta,
    # ── Solvency measures: three views ──────────────────────────────
    # (1) With full franchise (DSSW view)
    E_MV             = total_equity - all_asset_loss,
    E_MV_F           = E_MV + F_total,
    emv_f_ta         = 100 * safe_div(E_MV_F, total_asset),
    # (2) Post-uninsured-run (key measure in the paper)
    E_MV_F_insured   = E_MV + F_insured,
    v_postrun        = E_MV_F_insured,
    v_postrun_ta     = 100 * safe_div(v_postrun, total_asset),
    # (3) Without franchise (naive mark-to-market)
    emv_ta           = 100 * safe_div(E_MV, total_asset),
    # Liquidity & coverage
    omo_mv = pmax(omo_eligible - loss_omo_sec, 0),
    rho    = safe_div(cash + omo_mv, uninsured_deposit),
    rho_0  = safe_div(cash + omo_eligible, uninsured_deposit),
    rho_C  = safe_div(cash, uninsured_deposit),
    rho_S  = safe_div(omo_mv, uninsured_deposit),
    liq_gap        = pmax(phi_star_baseline * uninsured_deposit -
                          (cash + omo_mv), 0),
    liq_gap_ta     = 100 * safe_div(liq_gap, total_asset),
    has_gap        = as.integer(liq_gap > 0),
    ell_x_uninsured = ell * du_ta,
    # Negative solvency flags
    emv_neg       = as.integer(E_MV      < 0),
    emv_f_neg     = as.integer(E_MV_F    < 0),
    v_postrun_neg = as.integer(v_postrun < 0),
    # Liabilities
    fhlb_ta      = fhlb_to_total_asset,
    wholesale_ta = fed_fund_purchase_to_total_asset + repo_to_total_asset +
                   fhlb_to_total_asset + other_borr_to_total_asset,
    # Convenience
    ta_billion    = total_asset / 1e6,
    mu_pct        = 100 * mu,
    loan_to_dep   = 100 * safe_div(total_loan, total_deposit),
    di_ta         = 100 * safe_div(insured_deposit, total_asset, 0)
  )

cat(sprintf("2022Q4 cross-section: %s banks (excl. GSIBs, failed)\n", fmt(nrow(df))))
## 2022Q4 cross-section: 4,696 banks (excl. GSIBs, failed)
cat(sprintf("DSSW merge: %s matched (%.1f%%)\n",
            fmt(sum(!is.na(df$beta_overall))),
            100 * mean(!is.na(df$beta_overall))))
## DSSW merge: 4,602 matched (98.0%)

2.4 Period-tagged loan tables

btfp_clean <- btfp_raw %>% filter(failed_bank == 0) %>%
  mutate(sub_period = assign_period(btfp_loan_date))
dw_clean   <- dw_raw   %>% filter(failed_bank == 0) %>%
  mutate(sub_period = assign_period(dw_loan_date))

dw_pf <- dw_clean %>%
  filter(dw_loan_date >= P0_start, dw_loan_date <= P3_end) %>%
  mutate(period = assign_period(dw_loan_date),
         rssd_id = as.character(rssd_id))

btfp_pf <- btfp_clean %>%
  filter(btfp_loan_date >= P2_start, btfp_loan_date <= P3_end) %>%
  mutate(period = assign_period(btfp_loan_date),
         rssd_id = as.character(rssd_id))

all_borrow <- bind_rows(
  dw_pf   %>% select(rssd_id, period),
  btfp_pf %>% select(rssd_id, period)
)

prior_set <- function(p) {
  i <- match(p, period_order)
  if (i == 1) return(character(0))
  unique(all_borrow$rssd_id[all_borrow$period %in% period_order[seq_len(i - 1)]])
}

banks_in_cell <- function(p, f) {
  data <- if (f == "DW") dw_pf else btfp_pf
  unique(data$rssd_id[data$period == p])
}

2.5 Classify borrowers and merge loan aggregates

btfp_crisis <- btfp_clean %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= CRISIS_END)
dw_crisis <- dw_clean %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= CRISIS_END)

btfp_bank <- btfp_crisis %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_amount           = sum(btfp_loan_amount, na.rm = TRUE) / 1000,
    btfp_n_loans          = n(),
    btfp_mean_term        = mean(btfp_term, na.rm = TRUE),
    btfp_mean_effmat      = mean(btfp_effective_maturity_days, na.rm = TRUE),
    btfp_mean_rate        = mean(btfp_interest_rate, na.rm = TRUE),
    btfp_total_collateral = sum(btfp_total_collateral, na.rm = TRUE) / 1000,
    .groups = "drop"
  ) %>% rename(idrssd = rssd_id)

dw_bank <- dw_crisis %>%
  group_by(rssd_id) %>%
  summarise(
    dw_amount            = sum(dw_loan_amount, na.rm = TRUE) / 1000,
    dw_n_loans           = n(),
    dw_mean_term         = mean(dw_term, na.rm = TRUE),
    dw_mean_effmat       = mean(dw_effective_maturity_days, na.rm = TRUE),
    dw_mean_rate         = mean(dw_interest_rate, na.rm = TRUE),
    dw_total_collateral  = sum(dw_total_collateral, na.rm = TRUE) / 1000,
    dw_omo_collateral    = sum(dw_omo_eligible, na.rm = TRUE) / 1000,
    dw_nonomo_collateral = sum(dw_non_omo_eligible, na.rm = TRUE) / 1000,
    .groups = "drop"
  ) %>% rename(idrssd = rssd_id)

df <- df %>%
  left_join(btfp_bank, by = "idrssd") %>%
  left_join(dw_bank,   by = "idrssd") %>%
  mutate(
    btfp_amount  = replace_na(btfp_amount, 0),
    dw_amount    = replace_na(dw_amount,   0),
    btfp_n_loans = replace_na(btfp_n_loans, 0L),
    dw_n_loans   = replace_na(dw_n_loans,   0L),
    used_btfp = as.integer(btfp_amount > 0),
    used_dw   = as.integer(dw_amount   > 0),
    borrowed  = as.integer(used_btfp == 1 | used_dw == 1),
    borrower_type = dplyr::case_when(
      used_btfp == 1 & used_dw == 1 ~ "Both",
      used_btfp == 1                ~ "BTFP Only",
      used_dw   == 1                ~ "DW Only",
      TRUE                          ~ "Non-Borrower"
    ),
    total_borrow   = btfp_amount + dw_amount,
    borrow_ta      = 100 * safe_div(total_borrow, total_asset),
    log_borrow     = ifelse(total_borrow > 0, log(total_borrow), NA_real_)
  )

cat(sprintf("\nBorrower counts (crisis %s to %s):\n", CRISIS_START, CRISIS_END))
## 
## Borrower counts (crisis 2023-03-08 to 2023-05-04):
for (t in c("Non-Borrower", "DW Only", "BTFP Only", "Both"))
  cat(sprintf("  %-13s: %s\n", t, fmt(sum(df$borrower_type == t))))
##   Non-Borrower : 3,836
##   DW Only      : 341
##   BTFP Only    : 412
##   Both         : 107
cat(sprintf("  %-13s: %s\n", "Total sample", fmt(nrow(df))))
##   Total sample : 4,696

2.6 Regression-ready variables

reg_vars <- c(
  "f_ta", "fu_ta", "fi_ta", "emv_ta", "v_postrun_ta", "emv_f_ta",
  "ell", "ell_sec", "ell_omo", "ell_nonomo", "ell_loan",
  "rho", "rho_0", "rho_C", "rho_S", "liq_gap_ta",
  "eq_ta", "cash_ta", "sec_ta", "omo_ta", "loan_ta", "dep_ta",
  "du_ta", "di_ta", "mu", "wholesale_ta",
  "log_ta", "loan_to_dep", "roa", "borrow_ta"
)
df <- wz_cols(df, reg_vars)

cat(sprintf("Regression transforms: %s winsorized+standardized vars\n",
            fmt(sum(paste0(reg_vars, "_w_z") %in% names(df)))))
## Regression transforms: 30 winsorized+standardized vars

2.7 Arbitrage-window sample

btfp_arb_bank <- btfp_clean %>%
  filter(btfp_loan_date >= ARB_START, btfp_loan_date <= ARB_END) %>%
  group_by(rssd_id) %>%
  summarise(btfp_arb_amount = sum(btfp_loan_amount, na.rm = TRUE) / 1000,
            .groups = "drop") %>%
  rename(idrssd = rssd_id)

df_arb <- call_raw %>%
  filter(period == "2023Q3", gsib == 0, failed_bank == 0) %>%
  left_join(dssw_for_quarter("2022Q4"),       by = "idrssd") %>%
  left_join(dssw_costs_for_quarter("2022Q4"), by = "idrssd") %>%
  add_franchise_vars() %>%
  mutate(
    idrssd       = as.character(idrssd),
    
        # ── Size and ratios (pp) ──
    log_ta   = log(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,
    loan_ta  = total_loan_to_total_asset,
    dep_ta   = total_deposit_to_total_asset,
    du_ta    = uninsured_leverage,
    mu       = safe_div(uninsured_deposit, total_deposit, 0),

    # ── MTM losses / assets (pp) ──
    ell        = mtm_loss_to_total_asset,
    ell_sec    = loss_total_sec_to_ta,
    ell_omo    = loss_omo_sec_to_ta,
    ell_nonomo = loss_nonomo_sec_to_ta,
    ell_loan   = loss_total_loan_to_ta,

    
    E_MV         = total_equity - all_asset_loss,
    v_postrun    = E_MV + F_insured,
    v_postrun_ta = 100 * safe_div(v_postrun, total_asset),
    eq_ta        = book_equity_to_total_asset,
    cash_ta      = cash_to_total_asset,
    du_ta        = uninsured_leverage,
    log_ta       = log(total_asset),
    loan_to_dep  = 100 * safe_div(total_loan, total_deposit),
    wholesale_ta = fed_fund_purchase_to_total_asset + repo_to_total_asset +
                   fhlb_to_total_asset + other_borr_to_total_asset,
    roa          = roa
  ) %>%
  left_join(btfp_arb_bank, by = "idrssd") %>%
  mutate(
    btfp_arb_amount = replace_na(btfp_arb_amount, 0),
    borrowed_arb    = as.integer(btfp_arb_amount > 0)
  )

df_arb <- wz_cols(df_arb, c("ell", "du_ta", "log_ta", "cash_ta",
                             "loan_to_dep", "eq_ta", "wholesale_ta", "roa"))

cat(sprintf("Arbitrage sample: %s banks, %s borrowers\n",
            fmt(nrow(df_arb)), fmt(sum(df_arb$borrowed_arb))))
## Arbitrage sample: 4,604 banks, 766 borrowers

3. Descriptive Tables

3.1 Table 1 — Emergency Borrowing by Sub-Period × Facility

The refined Table 1 adds three maturity-band rows that tell the DW-liquidity vs BTFP-solvency story in one glance:

  • <5 days: emergency overnight liquidity — routine DW use, liquid-but-stressed banks
  • 5–90 days: within the DW contractual maximum — banks wanting term funding for weeks
  • >90 days: beyond the DW maximum — BTFP’s unique role as a term solvency tool
# ── Per-loan helper columns (maturity bands, early repayment) ──────────
add_loan_fields <- function(data, amt, coll, term, repay, mat,
                            held, rate, facility_name, date_col) {
  data %>%
    mutate(
      amt_b    = !!sym(amt)  / 1e9,                    # loan principal, $B
      coll_b   = !!sym(coll) / 1e9,
      loan_to_coll = safe_div(!!sym(amt), !!sym(coll)),
      term_days    = !!sym(term),
      held_days    = !!sym(held),
      rate_pct     = !!sym(rate),
      mat_band     = dplyr::case_when(
        term_days <  5             ~ "<5 days",
        term_days >= 5 & term_days <= 90 ~ "5-90 days",
        term_days >  90            ~ ">90 days",
        TRUE ~ NA_character_
      ),
      early_repay = as.integer(!is.na(!!sym(repay)) & !is.na(!!sym(mat)) &
                                !!sym(repay) < !!sym(mat)),
      facility = facility_name,
      loan_date = !!sym(date_col)
    )
}

dw_L   <- add_loan_fields(dw_pf, "dw_loan_amount", "dw_total_collateral",
                          "dw_term", "dw_repayment_date", "dw_maturity_date",
                          "dw_effective_maturity_days", "dw_interest_rate",
                          "DW", "dw_loan_date")
btfp_L <- add_loan_fields(btfp_pf, "btfp_loan_amount", "btfp_total_collateral",
                          "btfp_term", "btfp_repayment_date", "btfp_maturity_date",
                          "btfp_effective_maturity_days", "btfp_interest_rate",
                          "BTFP", "btfp_loan_date")

loans_all <- bind_rows(
  dw_L   %>% select(period, facility, rssd_id, amt_b, coll_b, loan_to_coll,
                    term_days, held_days, rate_pct, mat_band, early_repay),
  btfp_L %>% select(period, facility, rssd_id, amt_b, coll_b, loan_to_coll,
                    term_days, held_days, rate_pct, mat_band, early_repay)
)

# ── Build each cell's stats ────────────────────────────────────────────
build_cell_stats <- function(p, f) {
  loans <- loans_all %>% filter(period == p, facility == f)
  if (nrow(loans) == 0)
    return(tibble(col_id = paste(p, f, sep = " | ")))
  banks_cell <- unique(loans$rssd_id)
  n_banks <- length(banks_cell)
  prior <- prior_set(p)
  n_new    <- sum(!(banks_cell %in% prior))
  n_return <- sum(banks_cell %in% prior)

  per_bank <- loans %>%
    group_by(rssd_id) %>%
    summarise(bank_total = sum(amt_b, na.rm = TRUE), .groups = "drop")
  total_b  <- sum(per_bank$bank_total, na.rm = TRUE)
  mean_m   <- mean(per_bank$bank_total, na.rm = TRUE) * 1000
  median_m <- median(per_bank$bank_total, na.rm = TRUE) * 1000

  tibble(
    col_id                  = paste(p, f, sep = " | "),
    `N (Loans)`             = nrow(loans),
    `N (Banks)`             = n_banks,
    `New banks`             = n_new,
    `Returning banks`       = n_return,
    `Total borrow ($B)`     = total_b,
    `Mean borrow ($M)`      = mean_m,
    `Median borrow ($M)`    = median_m,
    `Loan/Coll (aggregate)` = sum(loans$amt_b, na.rm = TRUE) /
                              pmax(sum(loans$coll_b, na.rm = TRUE), 1e-12),
    `Term — mean (days)`    = mean(loans$term_days, na.rm = TRUE),
    `Term — median (days)`  = median(loans$term_days, na.rm = TRUE),
    # ── Maturity bands (NEW) ──
    `% Term <5 days`        = 100 * mean(loans$mat_band == "<5 days",  na.rm = TRUE),
    `% Term 5-90 days`      = 100 * mean(loans$mat_band == "5-90 days", na.rm = TRUE),
    `% Term >90 days`       = 100 * mean(loans$mat_band == ">90 days",  na.rm = TRUE),
    `% Early repayment`     = 100 * mean(loans$early_repay, na.rm = TRUE),
    `Held days — mean`      = mean(loans$held_days, na.rm = TRUE),
    `Held days — median`    = median(loans$held_days, na.rm = TRUE),
    `Rate (%)`              = mean(loans$rate_pct, na.rm = TRUE)
  )
}

cell_stats <- col_specs %>%
  rowwise() %>%
  mutate(stats = list(build_cell_stats(period, facility))) %>%
  pull(stats) %>% bind_rows()

# Pivot: stats × (period × facility)
table1 <- cell_stats %>%
  pivot_longer(-col_id, names_to = "Statistic", values_to = "value") %>%
  pivot_wider(names_from = col_id, values_from = value) %>%
  mutate(Statistic = factor(Statistic, levels = c(
    "N (Loans)", "N (Banks)", "New banks", "Returning banks",
    "Total borrow ($B)", "Mean borrow ($M)", "Median borrow ($M)",
    "Loan/Coll (aggregate)",
    "Term — mean (days)", "Term — median (days)",
    "% Term <5 days", "% Term 5-90 days", "% Term >90 days",
    "% Early repayment",
    "Held days — mean", "Held days — median",
    "Rate (%)"
  ))) %>% arrange(Statistic)

print(table1, n = Inf)
## # A tibble: 17 × 7
##    Statistic     `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <fct>                         <dbl>               <dbl>                 <dbl>
##  1 N (Loans)                   171                 124                  1492    
##  2 N (Banks)                    72                  68                   406    
##  3 New banks                    72                  33                   340    
##  4 Returning ba…                 0                  35                    66    
##  5 Total borrow…                 9.46               10.1                 518.   
##  6 Mean borrow …               131.                149.                 1277.   
##  7 Median borro…                 9.75                8.98                  0.5  
##  8 Loan/Coll (a…                 0.172               0.176                 0.329
##  9 Term — mean …                 5.25                3.84                  4.41 
## 10 Term — media…                 1                   1                     1    
## 11 % Term <5 da…                86.5                87.1                  88.0  
## 12 % Term 5-90 …                13.5                12.9                  11.9  
## 13 % Term >90 d…                 0                   0                     0.134
## 14 % Early repa…                 6.43                5.65                  4.83 
## 15 Held days — …                 4.08                3.02                  3.01 
## 16 Held days — …                 1                   1                     1    
## 17 Rate (%)                      4.75                4.75                  4.92 
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <dbl>, `P3: FRC Week | DW` <dbl>,
## #   `P3: FRC Week | BTFP` <dbl>
save_kbl_latex(table1, "table1_emergency_borrowing",
               caption = "Emergency borrowing by sub-period × facility, with maturity-band decomposition.")

kbl(table1, digits = 3, align = c("l", rep("r", 6)),
    caption = "Table 1: Emergency Borrowing by Sub-Period × Facility") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 11) %>%
  pack_rows("Counts",           1, 4) %>%
  pack_rows("Dollar volumes",   5, 7) %>%
  pack_rows("Collateral",       8, 8) %>%
  pack_rows("Term structure",   9, 13) %>%
  pack_rows("Repayment",        14, 16) %>%
  pack_rows("Pricing",          17, 17) %>%
  footnote(general = paste(
    "Maturity bands are defined at origination. <5 days captures",
    "overnight/emergency liquidity. 5-90 days captures term funding within the",
    "Discount Window's contractual maximum. >90 days captures funding beyond",
    "the DW maximum — a range where only BTFP operates. BTFP became operational",
    "on March 13, 2023 (start of P2)."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
Table 1: Emergency Borrowing by Sub-Period × Facility
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP
Counts
N (Loans) 171.000 124.000 1492.000 1032.000 213.000 279.000
N (Banks) 72.000 68.000 406.000 472.000 101.000 184.000
New banks 72.000 33.000 340.000 442.000 34.000 54.000
Returning banks 0.000 35.000 66.000 30.000 67.000 130.000
Dollar volumes
Total borrow (\(B) </td> <td style="text-align:right;"> 9.455 </td> <td style="text-align:right;"> 10.102 </td> <td style="text-align:right;"> 518.485 </td> <td style="text-align:right;"> 122.358 </td> <td style="text-align:right;"> 9.613 </td> <td style="text-align:right;"> 11.326 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Mean borrow (\)M) 131.324 148.565 1277.056 259.232 95.175 61.553
Median borrow ($M) 9.750 8.977 0.500 20.000 4.000 12.850
Collateral
Loan/Coll (aggregate) 0.172 0.176 0.329 0.406 0.146 0.294
Term structure
Term — mean (days) 5.251 3.839 4.405 313.568 3.831 322.305
Term — median (days) 1.000 1.000 1.000 365.000 1.000 365.000
% Term <5 days 86.550 87.097 88.003 10.659 90.610 7.527
% Term 5-90 days 13.450 12.903 11.863 2.907 9.390 3.584
% Term >90 days 0.000 0.000 0.134 86.434 0.000 88.889
Repayment
% Early repayment 6.433 5.645 4.826 64.922 2.817 60.932
Held days — mean 4.076 3.016 3.005 151.043 2.784 177.219
Held days — median 1.000 1.000 1.000 14.500 1.000 222.000
Pricing
Rate (%) 4.750 4.750 4.920 4.678 5.050 4.788
Notes: Maturity bands are defined at origination. <5 days captures overnight/emergency liquidity. 5-90 days captures term funding within the Discount Window’s contractual maximum. >90 days captures funding beyond the DW maximum — a range where only BTFP operates. BTFP became operational on March 13, 2023 (start of P2).

3.2 Table 2 — Borrower Characteristics vs Non-Borrowers

Redesigned structure: - Panel A — Theory-relevant variables (what the DSSW/ours prediction depends on) - Panel B — Standard bank controls (regression covariates) - New “Diff vs Never” column with t-test stars

# Safe t-test wrapper
t_diff_star <- function(x, y) {
  x <- x[is.finite(x)]; y <- y[is.finite(y)]
  if (length(x) < 3 || length(y) < 3) return(list(diff = NA_real_, p = NA_real_))
  t <- suppressWarnings(t.test(x, y))
  list(diff = mean(x, na.rm = TRUE) - mean(y, na.rm = TRUE), p = t$p.value)
}

nonborr <- df %>% filter(borrower_type == "Non-Borrower")

# Cell bank set (all rssd_ids that appear in the cell's loan set)
cell_bank_df <- function(p, f) {
  ids <- banks_in_cell(p, f)
  df %>% filter(idrssd %in% ids)
}

# Variables to show, in order, labels and source columns
vars_panelA <- tribble(
  ~label,                         ~col,
  "Uninsured leverage (%)",       "du_ta",
  "MTM loss / TA (%)",            "ell",
  "OMO-Sec MTM loss / TA (%)", "ell_omo",
  "Liquidity buffer ρ (C+θS)/Du", "rho",
  "Franchise / TA (%)",           "f_ta",
  "v (post-run) / TA (%)",        "v_postrun_ta"
)

vars_panelB <- tribble(
  ~label,                         ~col,
  "Total assets ($B)",            "ta_billion",
  "Book equity / TA (%)",         "eq_ta",
  "Cash / TA (%)",                "cash_ta",
  "Loan / Deposit (%)",           "loan_to_dep",
  "Wholesale funding / TA (%)",   "wholesale_ta",
  "ROA (%)",                      "roa"
)

build_char_row <- function(var_col, label) {
  out <- tibble(Statistic = label)
  for (i in seq_len(nrow(col_specs))) {
    p <- col_specs$period[i]; f <- col_specs$facility[i]
    cid <- col_specs$col_id[i]
    cell <- cell_bank_df(p, f)
    m_val <- mean(cell[[var_col]], na.rm = TRUE)
    diff  <- t_diff_star(cell[[var_col]], nonborr[[var_col]])
    out[[cid]] <- sprintf("%.2f%s", m_val, stars_pval(diff$p))
  }
  out[["Non-Borrower (N=3,815)"]] <- sprintf("%.2f", mean(nonborr[[var_col]], na.rm = TRUE))
  out
}

panelA <- map2_dfr(vars_panelA$col, vars_panelA$label, build_char_row)
panelB <- map2_dfr(vars_panelB$col, vars_panelB$label, build_char_row)

# Add bank counts at top
bank_counts <- tibble(Statistic = "N (banks)")
for (i in seq_len(nrow(col_specs))) {
  bank_counts[[col_specs$col_id[i]]] <- fmt(
    nrow(cell_bank_df(col_specs$period[i], col_specs$facility[i])))
}
bank_counts[["Non-Borrower (N=3,815)"]] <- fmt(nrow(nonborr))

table2 <- bind_rows(bank_counts, panelA, panelB)
print(table2, n = Inf)
## # A tibble: 13 × 8
##    Statistic     `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 N (banks)     72                    67                  397                  
##  2 Uninsured le… 26.87***              29.52***            28.45***             
##  3 MTM loss / T… 9.33*                 8.92                9.03***              
##  4 OMO-Sec MTM … 1.39**                1.68***             1.65***              
##  5 Liquidity bu… 0.62***               0.57***             1.09**               
##  6 Franchise / … 12.42***              11.94***            12.54***             
##  7 v (post-run)… 9.28***               8.51***             9.43***              
##  8 Total assets… 4.40*                 7.59***             6.51***              
##  9 Book equity … 9.05***               8.41***             8.89***              
## 10 Cash / TA (%) 4.38***               3.77***             6.08***              
## 11 Loan / Depos… 76.00                 78.28***            75.73***             
## 12 Wholesale fu… 5.65***               6.72***             5.04***              
## 13 ROA (%)       1.03*                 1.04*               1.16                 
## # ℹ 4 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>, `Non-Borrower (N=3,815)` <chr>
save_kbl_latex(table2, "table2_borrower_characteristics",
               caption = "Borrower vs non-borrower characteristics at 2022Q4. Stars from t-tests vs non-borrower mean.")

n_panelA <- nrow(panelA); n_panelB <- nrow(panelB)
kbl(table2, align = c("l", rep("r", 7)),
    caption = "Table 2: Borrower Characteristics by Cell vs Non-Borrowers (2022Q4)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 11) %>%
  pack_rows("Panel A. Theory-relevant variables", 2, 1 + n_panelA) %>%
  pack_rows("Panel B. Standard bank controls",
            2 + n_panelA, 1 + n_panelA + n_panelB) %>%
  footnote(general = paste(
    "Significance stars compare cell mean vs non-borrower mean via two-sided",
    "t-test. Liquidity buffer ρ = (Cash + MV of OMO-eligible securities) /",
    "Uninsured deposits. Franchise f/TA and v/TA are Option A (DSSW) measures",
    "using estimated uninsured deposit betas. * p<0.10, ** p<0.05, *** p<0.01."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
Table 2: Borrower Characteristics by Cell vs Non-Borrowers (2022Q4)
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP Non-Borrower (N=3,815)
N (banks) 72 67 397 460 101 182 3,836
Panel A. Theory-relevant variables
Uninsured leverage (%) 26.87*** 29.52*** 28.45*** 27.42*** 25.96*** 26.89*** 22.40
MTM loss / TA (%) 9.33* 8.92 9.03*** 9.61*** 8.78 9.90*** 8.40
OMO-Sec MTM loss / TA (%) 1.39** 1.68*** 1.65*** 1.78*** 1.51** 1.76*** 1.05
Liquidity buffer ρ (C+θS)/Du 0.62*** 0.57*** 1.09** 0.78*** 0.75*** 0.73*** 2.90
Franchise / TA (%) 12.42*** 11.94*** 12.54*** 12.78*** 12.53*** 13.23 13.25
v (post-run) / TA (%) 9.28*** 8.51*** 9.43*** 8.43*** 10.12*** 8.35*** 12.64
Panel B. Standard bank controls
Total assets ($B) 4.40* 7.59*** 6.51*** 6.35*** 2.76 5.12 1.70
Book equity / TA (%) 9.05*** 8.41*** 8.89*** 8.16*** 8.99*** 8.10*** 11.38
Cash / TA (%) 4.38*** 3.77*** 6.08*** 4.68*** 5.34*** 4.19*** 9.55
Loan / Deposit (%) 76.00 78.28*** 75.73*** 72.96 76.20** 70.95 71.94
Wholesale funding / TA (%) 5.65*** 6.72*** 5.04*** 5.48*** 5.33*** 5.09*** 3.33
ROA (%) 1.03* 1.04* 1.16 1.07* 1.15 1.01* 1.42
Notes: Significance stars compare cell mean vs non-borrower mean via two-sided t-test. Liquidity buffer ρ = (Cash + MV of OMO-eligible securities) / Uninsured deposits. Franchise f/TA and v/TA are Option A (DSSW) measures using estimated uninsured deposit betas. * p<0.10, ** p<0.05, *** p<0.01.

3.3 Table 3 — Solvency Three Views

Three solvency panels to reveal how solvency depends on the franchise assumption: - Panel A (with full franchise): E^{MV} + F_total — DSSW’s view. Almost all banks solvent. - Panel B (post-uninsured run): v = E^{MV} + F_insured — paper’s v measure. - Panel C (without franchise): E^{MV} = equity − MTM loss — naive mark-to-market.

solvency_stats <- function(col_var, neg_flag, data) {
  tibble(
    mean_pct     = mean(data[[col_var]],   na.rm = TRUE),
    median_pct   = median(data[[col_var]], na.rm = TRUE),
    share_neg    = 100 * mean(data[[neg_flag]], na.rm = TRUE)
  )
}

build_solvency_panel <- function(measure_col, neg_col, panel_label) {
  out <- tibble(
    Statistic = c(sprintf("%s mean (%%)",   panel_label),
                  sprintf("%s median (%%)", panel_label),
                  sprintf("%% with %s < 0", panel_label))
  )
  for (i in seq_len(nrow(col_specs))) {
    p <- col_specs$period[i]; f <- col_specs$facility[i]
    cell <- cell_bank_df(p, f)
    s <- solvency_stats(measure_col, neg_col, cell)
    out[[col_specs$col_id[i]]] <- c(
      sprintf("%.2f", s$mean_pct),
      sprintf("%.2f", s$median_pct),
      sprintf("%.2f", s$share_neg)
    )
  }
  s_nb <- solvency_stats(measure_col, neg_col, nonborr)
  out[["Non-Borrower"]] <- c(
    sprintf("%.2f", s_nb$mean_pct),
    sprintf("%.2f", s_nb$median_pct),
    sprintf("%.2f", s_nb$share_neg)
  )
  out
}

solv_A <- build_solvency_panel("emv_f_ta",     "emv_f_neg",     "E^MV + F_total / TA")
solv_B <- build_solvency_panel("v_postrun_ta", "v_postrun_neg", "v = E^MV + F_insured / TA")
solv_C <- build_solvency_panel("emv_ta",       "emv_neg",       "E^MV / TA")

# N row
n_row <- tibble(Statistic = "N (banks)")
for (i in seq_len(nrow(col_specs))) {
  n_row[[col_specs$col_id[i]]] <- fmt(
    nrow(cell_bank_df(col_specs$period[i], col_specs$facility[i])))
}
n_row[["Non-Borrower"]] <- fmt(nrow(nonborr))

table3 <- bind_rows(n_row, solv_A, solv_B, solv_C)
print(table3, n = Inf)
## # A tibble: 10 × 8
##    Statistic     `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 N (banks)     72                    67                  397                  
##  2 E^MV + F_tot… 12.14                 11.42               12.52                
##  3 E^MV + F_tot… 11.66                 11.45               12.49                
##  4 % with E^MV … 0.00                  0.00                1.02                 
##  5 v = E^MV + F… 9.28                  8.51                9.43                 
##  6 v = E^MV + F… 8.58                  8.42                9.43                 
##  7 % with v = E… 1.39                  1.49                3.81                 
##  8 E^MV / TA me… -0.27                 -0.52               -0.13                
##  9 E^MV / TA me… -0.84                 -1.18               -0.23                
## 10 % with E^MV … 59.72                 58.21               51.89                
## # ℹ 4 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>, `Non-Borrower` <chr>
save_kbl_latex(table3, "table3_solvency_three_views",
               caption = "Solvency under three franchise views: with full franchise, post-uninsured run, and without franchise.")

kbl(table3, align = c("l", rep("r", 7)),
    caption = "Table 3: Solvency by Franchise Assumption") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 11) %>%
  pack_rows("Panel A. With full franchise (E^MV + F_total)",           2, 4) %>%
  pack_rows("Panel B. Post uninsured run (v = E^MV + F_ins)", 5, 7) %>%
  pack_rows("Panel C. Without franchise (E^MV = E - MTM)",         8, 10) %>%
  footnote(general = paste(
    "Panel A is DSSW's solvency measure where the full deposit franchise is",
    "preserved. Panel B measures fundamental value after an uninsured run",
    "destroys the uninsured franchise, retaining only insured franchise value.",
    "Panel C is the naive mark-to-market equity without any franchise benefit.",
    "The % with value < 0 row reveals how many banks become technically",
    "insolvent under each view."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
Table 3: Solvency by Franchise Assumption
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP Non-Borrower
N (banks) 72 67 397 460 101 182 3,836
Panel A. With full franchise (E^MV + F_total)
E^MV + F_total / TA mean (%) 12.14 11.42 12.52 11.43 12.87 11.42 15.18
E^MV + F_total / TA median (%) 11.66 11.45 12.49 11.58 12.82 11.63 14.61
% with E^MV + F_total / TA < 0 0.00 0.00 1.02 0.66 1.00 0.55 0.51
Panel B. Post uninsured run (v = E^MV + F_ins)
v = E^MV + F_insured / TA mean (%) 9.28 8.51 9.43 8.43 10.12 8.35 12.64
v = E^MV + F_insured / TA median (%) 8.58 8.42 9.43 8.63 9.59 8.71 11.99
% with v = E^MV + F_insured / TA < 0 1.39 1.49 3.81 4.40 2.00 5.49 1.41
Panel C. Without franchise (E^MV = E - MTM)
E^MV / TA mean (%) -0.27 -0.52 -0.13 -1.45 0.22 -1.81 2.77
E^MV / TA median (%) -0.84 -1.18 -0.23 -1.08 0.04 -1.39 1.52
% with E^MV / TA < 0 59.72 58.21 51.89 58.91 48.51 59.89 40.34
Notes: Panel A is DSSW’s solvency measure where the full deposit franchise is preserved. Panel B measures fundamental value after an uninsured run destroys the uninsured franchise, retaining only insured franchise value. Panel C is the naive mark-to-market equity without any franchise benefit. The % with value < 0 row reveals how many banks become technically insolvent under each view.

3.4 Table 4 — Concentration of Borrowing

New table making the concentration finding the headline empirical fact:

build_concentration <- function() {
  out <- tibble(Statistic = c(
    "N banks in cell",
    "Top 3 banks' share of $ (%)",
    "Top 10 banks' share of $ (%)",
    "Top decile share of $ (%)",
    "HHI",
    "Median borrow ($M)",
    "Mean borrow ($M)",
    "Mean/Median ratio"
  ))
  for (i in seq_len(nrow(col_specs))) {
    p <- col_specs$period[i]; f <- col_specs$facility[i]
    cid <- col_specs$col_id[i]
    loans <- loans_all %>% filter(period == p, facility == f)
    if (nrow(loans) == 0) {
      out[[cid]] <- rep(NA_character_, 8); next
    }
    per_bank <- loans %>%
      group_by(rssd_id) %>%
      summarise(b = sum(amt_b, na.rm = TRUE), .groups = "drop")
    total <- sum(per_bank$b)
    shares <- per_bank$b / total
    sorted <- sort(per_bank$b, decreasing = TRUE)
    n <- nrow(per_bank)
    decile_n <- max(ceiling(n * 0.10), 1)
    top3 <- sum(head(sorted, 3)) / total * 100
    top10 <- sum(head(sorted, 10)) / total * 100
    top_dec <- sum(head(sorted, decile_n)) / total * 100
    hhi <- sum(shares^2)
    med <- median(per_bank$b) * 1000
    mean_val <- mean(per_bank$b) * 1000
    out[[cid]] <- c(
      fmt(n),
      sprintf("%.1f", top3),
      sprintf("%.1f", top10),
      sprintf("%.1f", top_dec),
      sprintf("%.3f", hhi),
      sprintf("%.2f", med),
      sprintf("%.2f", mean_val),
      sprintf("%.1f×", mean_val / pmax(med, 1e-9))
    )
  }
  out
}

table4 <- build_concentration()
print(table4, n = Inf)
## # A tibble: 8 × 7
##   Statistic      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##   <chr>          <chr>                 <chr>               <chr>                
## 1 N banks in ce… 72                    68                  406                  
## 2 Top 3 banks' … 48.8                  42.6                81.4                 
## 3 Top 10 banks'… 83.8                  84.2                94.3                 
## 4 Top decile sh… 77.5                  73.6                99.1                 
## 5 HHI            0.102                 0.096               0.338                
## 6 Median borrow… 9.75                  8.98                0.50                 
## 7 Mean borrow (… 131.32                148.56              1277.06              
## 8 Mean/Median r… 13.5×                 16.5×               2554.1×              
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(table4, "table4_concentration",
               caption = "Concentration of emergency borrowing within each cell.")

kbl(table4, align = c("l", rep("r", 6)),
    caption = "Table 4: Borrowing Concentration by Cell") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 11) %>%
  footnote(general = paste(
    "Top-3, Top-10, and top-decile shares show cumulative dollar-borrowing",
    "concentration within each cell. HHI is the Herfindahl-Hirschman Index",
    "of within-cell borrowing shares; values above 0.25 indicate high",
    "concentration. A large mean/median ratio indicates extreme right-skew",
    "in borrowing volumes."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
Table 4: Borrowing Concentration by Cell
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP
N banks in cell 72 68 406 472 101 184
Top 3 banks’ share of $ (%) 48.8 42.6 81.4 46.4 52.1 23.8
Top 10 banks’ share of $ (%) 83.8 84.2 94.3 65.0 82.3 57.3
Top decile share of $ (%) 77.5 73.6 99.1 87.7 84.2 71.0
HHI 0.102 0.096 0.338 0.120 0.135 0.039
Median borrow (\(M) </td> <td style="text-align:right;"> 9.75 </td> <td style="text-align:right;"> 8.98 </td> <td style="text-align:right;"> 0.50 </td> <td style="text-align:right;"> 20.00 </td> <td style="text-align:right;"> 4.00 </td> <td style="text-align:right;"> 12.85 </td> </tr> <tr> <td style="text-align:left;"> Mean borrow (\)M) 131.32 148.56 1277.06 259.23 95.17 61.55
Mean/Median ratio 13.5× 16.5× 2554.1× 13.0× 23.8× 4.8×
Notes: Top-3, Top-10, and top-decile shares show cumulative dollar-borrowing concentration within each cell. HHI is the Herfindahl-Hirschman Index of within-cell borrowing shares; values above 0.25 indicate high concentration. A large mean/median ratio indicates extreme right-skew in borrowing volumes.

3.5 Table 5 — Test-Borrower Classification (BTFP)

TEST_THRESHOLD <- 1e6

btfp_bank_summary <- btfp_clean %>%
  filter(btfp_loan_date >= P2_start, btfp_loan_date <= P3_end) %>%
  mutate(rssd_id = as.character(rssd_id)) %>%
  group_by(rssd_id) %>%
  summarise(
    n_loans_total = n(),
    n_test_loans  = sum(btfp_loan_amount < TEST_THRESHOLD),
    smallest_m    = min(btfp_loan_amount,  na.rm = TRUE) / 1e6,
    largest_m     = max(btfp_loan_amount,  na.rm = TRUE) / 1e6,
    total_all_m   = sum(btfp_loan_amount,  na.rm = TRUE) / 1e6,
    .groups = "drop"
  ) %>%
  mutate(tester_type = dplyr::case_when(
    largest_m  <  TEST_THRESHOLD / 1e6 ~ "Pure tester",
    smallest_m <  TEST_THRESHOLD / 1e6 ~ "Tested then borrowed",
    TRUE                                ~ "No tiny loans"
  ),
  tester_type = factor(tester_type,
    levels = c("Pure tester", "Tested then borrowed", "No tiny loans"))) %>%
  left_join(df %>% select(idrssd, total_asset, cash_ta, du_ta, ell,
                          eq_ta, wholesale_ta),
            by = c("rssd_id" = "idrssd"))

table5 <- btfp_bank_summary %>%
  group_by(tester_type) %>%
  summarise(
    `N (banks)`              = n(),
    `Mean TA ($B)`           = round(mean(total_asset / 1e6, na.rm = TRUE), 2),
    `Mean # BTFP loans`      = round(mean(n_loans_total), 1),
    `Mean smallest ($M)`     = round(mean(smallest_m, na.rm = TRUE), 2),
    `Mean largest ($M)`      = round(mean(largest_m,  na.rm = TRUE), 1),
    `Mean total ($M)`        = round(mean(total_all_m, na.rm = TRUE), 1),
    `Cash/TA (%)`            = round(mean(cash_ta,    na.rm = TRUE), 2),
    `Uninsured lev (%)`      = round(mean(du_ta,      na.rm = TRUE), 2),
    `MTM loss/TA (%)`        = round(mean(ell,        na.rm = TRUE), 2),
    `Equity/TA (%)`          = round(mean(eq_ta,      na.rm = TRUE), 2),
    `Wholesale/TA (%)`       = round(mean(wholesale_ta, na.rm = TRUE), 2),
    .groups = "drop"
  )

print(table5, n = Inf)
## # A tibble: 3 × 12
##   tester_type          `N (banks)` `Mean TA ($B)` `Mean # BTFP loans`
##   <fct>                      <int>          <dbl>               <dbl>
## 1 Pure tester                   46          24.9                  1  
## 2 Tested then borrowed          13           1.69                 7.5
## 3 No tiny loans                473           5.31                 2.5
## # ℹ 8 more variables: `Mean smallest ($M)` <dbl>, `Mean largest ($M)` <dbl>,
## #   `Mean total ($M)` <dbl>, `Cash/TA (%)` <dbl>, `Uninsured lev (%)` <dbl>,
## #   `MTM loss/TA (%)` <dbl>, `Equity/TA (%)` <dbl>, `Wholesale/TA (%)` <dbl>
save_kbl_latex(table5, "table5_test_borrowers",
               caption = "BTFP test-borrower classification with balance-sheet characteristics.")

kbl(table5, align = c("l", rep("r", 11)),
    caption = "Table 5: BTFP Test-Borrower Analysis") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 10) %>%
  footnote(general = paste(
    "Pure testers borrowed only tiny loans (under $1M) and never borrowed",
    "substantively. Tested-then-borrowed banks started with a test loan before",
    "taking real BTFP funds. No-tiny-loans banks never borrowed below $1M."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
Table 5: BTFP Test-Borrower Analysis
tester_type N (banks) Mean TA (\(B) </th> <th style="text-align:right;"> Mean # BTFP loans </th> <th style="text-align:right;"> Mean smallest (\)M) Mean largest (\(M) </th> <th style="text-align:right;"> Mean total (\)M) Cash/TA (%) Uninsured lev (%) MTM loss/TA (%) Equity/TA (%) Wholesale/TA (%)
Pure tester 46 24.94 1.0 0.06 0.1 0.1 7.56 31.49 8.31 9.17 2.96
Tested then borrowed 13 1.69 7.5 0.39 55.2 146.3 5.05 25.80 9.82 8.53 4.34
No tiny loans 473 5.31 2.5 53.47 109.1 278.6 4.40 26.91 9.70 8.08 5.55
Notes: Pure testers borrowed only tiny loans (under $1M) and never borrowed substantively. Tested-then-borrowed banks started with a test loan before taking real BTFP funds. No-tiny-loans banks never borrowed below $1M.

4. Plots

4.1 Figure 1 — Daily crisis borrowing (DW and BTFP)

daily_dw <- dw_clean %>%
  filter(dw_loan_date >= P0_start, dw_loan_date <= P3_end) %>%
  group_by(date = dw_loan_date) %>%
  summarise(DW = sum(dw_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop")

daily_btfp <- btfp_clean %>%
  filter(btfp_loan_date >= P0_start, btfp_loan_date <= P3_end) %>%
  group_by(date = btfp_loan_date) %>%
  summarise(BTFP = sum(btfp_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop")

daily <- full_join(daily_dw, daily_btfp, by = "date") %>%
  mutate(DW = replace_na(DW, 0), BTFP = replace_na(BTFP, 0)) %>%
  pivot_longer(c(DW, BTFP), names_to = "Facility", values_to = "amount")

key_events <- tibble(
  date  = as.Date(c("2023-03-10", "2023-03-12", "2023-03-13", "2023-05-01")),
  label = c("SVB failed", "Signature failed", "BTFP live + DW no-haircut",
            "FRC failed")
)

fig1 <- ggplot(daily, aes(x = date, y = amount, color = Facility)) +
  geom_line(linewidth = 0.8) +
  geom_vline(data = key_events, aes(xintercept = date),
             linetype = "dashed", color = "grey40", linewidth = 0.3) +
  geom_text(data = key_events, aes(x = date, y = Inf, label = label),
            inherit.aes = FALSE, size = 3, angle = 90, hjust = 1.1,
            vjust = 1.1, color = "grey30") +
  scale_color_manual(values = c(DW = "#E53935", BTFP = "#1565C0")) +
  scale_y_continuous(labels = label_dollar(suffix = "B")) +
  scale_x_date(date_breaks = "1 week", date_labels = "%b %d") +
  labs(title = "Daily emergency borrowing, crisis window",
       subtitle = "Mar 1 – May 4, 2023",
       x = NULL, y = "Daily principal originated",
       color = NULL) +
  theme_gp

print(fig1)

save_figure(fig1, "fig1_daily_crisis_borrowing", width = 12, height = 6)

4.2 Figure 2 — Distribution of borrowing (by-facility)

borrow_plot_df <- df %>%
  filter(borrower_type != "Non-Borrower") %>%
  mutate(borrow_m = total_borrow * 1000,   # thousands → $ / 1e3 → $M  (total_borrow is in thousands $)
         ta_b     = total_asset / 1e6)

# Panel A: cumulative borrowing, log x
figA <- ggplot(borrow_plot_df, aes(x = borrow_m, fill = borrower_type,
                                   color = borrower_type)) +
  geom_density(alpha = 0.35, linewidth = 0.6) +
  geom_rug(sides = "b", alpha = 0.3, length = unit(0.03, "npc")) +
  scale_x_log10(labels = label_dollar(suffix = "M", accuracy = 1),
                breaks = c(1, 10, 100, 1000, 10000)) +
  scale_fill_manual (values = fac_colors) +
  scale_color_manual(values = fac_colors) +
  facet_wrap(~ borrower_type, nrow = 1) +
  labs(title = "A. Cumulative crisis-window borrowing by facility",
       x = "Cumulative borrowing (log scale)", y = "Density") +
  theme_gp + theme(legend.position = "none")

# Panel B: borrower TA
figB <- ggplot(borrow_plot_df, aes(x = ta_b, fill = borrower_type,
                                   color = borrower_type)) +
  geom_density(alpha = 0.35, linewidth = 0.6) +
  geom_rug(sides = "b", alpha = 0.3, length = unit(0.03, "npc")) +
  scale_x_log10(labels = label_dollar(suffix = "B", accuracy = 0.1),
                breaks = c(0.1, 1, 10, 100, 1000)) +
  scale_fill_manual (values = fac_colors) +
  scale_color_manual(values = fac_colors) +
  facet_wrap(~ borrower_type, nrow = 1) +
  labs(title = "B. Borrower total assets (2022Q4) by facility",
       x = "Total assets (log scale)", y = "Density") +
  theme_gp + theme(legend.position = "none")

# Panel C: borrow/TA, capped
cap_C <- quantile(borrow_plot_df$borrow_ta, 0.95, na.rm = TRUE)
figC <- ggplot(borrow_plot_df %>% filter(borrow_ta <= cap_C),
               aes(x = borrow_ta, fill = borrower_type, color = borrower_type)) +
  geom_density(alpha = 0.35, linewidth = 0.6) +
  geom_rug(sides = "b", alpha = 0.3, length = unit(0.03, "npc")) +
  scale_x_continuous(labels = label_percent(scale = 1, accuracy = 1)) +
  scale_fill_manual (values = fac_colors) +
  scale_color_manual(values = fac_colors) +
  facet_wrap(~ borrower_type, nrow = 1) +
  labs(title = "C. Borrow / TA by facility",
       subtitle = sprintf("Capped at 95th pct (%.1f%%)", cap_C),
       x = "Cumulative borrowing / Total assets", y = "Density") +
  theme_gp + theme(legend.position = "none")

fig2 <- (figA / figB / figC)
print(fig2)

save_figure(fig2, "fig2_distribution_by_facility", width = 13, height = 11)

4.3 Figure 3 — Cumulative vs peak (rollover visual)

# Reconstruct daily outstanding for peak
dw_daily <- dw_clean %>%
  filter(!is.na(dw_repayment_date), dw_repayment_date > dw_loan_date,
         dw_loan_date >= P0_start, dw_loan_date <= P3_end) %>%
  mutate(rssd_id = as.character(rssd_id)) %>%
  transmute(rssd_id,
            active = map2(dw_loan_date, dw_repayment_date - 1,
                          ~ seq(.x, .y, by = "1 day")),
            amt = dw_loan_amount) %>%
  unnest(active) %>%
  group_by(rssd_id, date = active) %>%
  summarise(dw_out = sum(amt, na.rm = TRUE), .groups = "drop")

btfp_daily <- btfp_clean %>%
  filter(!is.na(btfp_repayment_date), btfp_repayment_date > btfp_loan_date,
         btfp_loan_date >= P2_start, btfp_loan_date <= P3_end) %>%
  mutate(rssd_id = as.character(rssd_id)) %>%
  transmute(rssd_id,
            active = map2(btfp_loan_date, btfp_repayment_date - 1,
                          ~ seq(.x, .y, by = "1 day")),
            amt = btfp_loan_amount) %>%
  unnest(active) %>%
  group_by(rssd_id, date = active) %>%
  summarise(btfp_out = sum(amt, na.rm = TRUE), .groups = "drop")

daily_combined <- full_join(dw_daily, btfp_daily, by = c("rssd_id", "date")) %>%
  mutate(dw_out = replace_na(dw_out, 0), btfp_out = replace_na(btfp_out, 0),
         total_out = dw_out + btfp_out)

peak_bank <- daily_combined %>%
  group_by(rssd_id) %>%
  summarise(peak_m = max(total_out, na.rm = TRUE) / 1e6, .groups = "drop")

scatter_df <- df %>%
  filter(borrower_type != "Non-Borrower") %>%
  mutate(rssd_id = idrssd, cum_m = total_borrow * 1000) %>%  # thousands → $M
  select(rssd_id, cum_m, borrower_type) %>%
  left_join(peak_bank, by = "rssd_id") %>%
  filter(cum_m > 0, peak_m > 0, is.finite(peak_m))

fig3 <- ggplot(scatter_df, aes(x = peak_m, y = cum_m, color = borrower_type)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed",
              color = "grey40", linewidth = 0.4) +
  geom_point(alpha = 0.45, size = 1.3) +
  scale_x_log10(labels = label_dollar(suffix = "M", accuracy = 1)) +
  scale_y_log10(labels = label_dollar(suffix = "M", accuracy = 1)) +
  scale_color_manual(values = fac_colors, name = NULL) +
  labs(title = "Cumulative borrowing vs peak outstanding balance",
       subtitle = "Distance above 45° line = rollover intensity",
       x = "Peak daily outstanding balance (log)",
       y = "Cumulative borrowing, whole crisis (log)") +
  theme_gp

print(fig3)

save_figure(fig3, "fig3_cumulative_vs_peak", width = 9, height = 7)

Table 6 — New vs Returning borrowers

# ══════════════════════════════════════════════════════════════════════
# Helper: build two-panel table (Panel A type1, Panel B type2)
# Used for New/Returning and One-time/Repeat decompositions
# ══════════════════════════════════════════════════════════════════════

# Variables to show in each two-panel table (same list for both)
two_panel_vars <- tribble(
  ~label,                         ~col,
  "N (banks)",                    "N",                     # special-cased
  "Total assets ($B)",            "ta_billion",
  "Cash / TA (%)",                "cash_ta",
  "Securities / TA (%)",          "sec_ta",
  "Loans / TA (%)",               "loan_ta",
  "Book equity / TA (%)",         "eq_ta",
  "Uninsured / Deposits (%)",     "mu_pct",
  "Uninsured leverage (%)",       "du_ta",
  "Wholesale funding / TA (%)",   "wholesale_ta",
  "FHLB / TA (%)",                "fhlb_ta",
  "ROA (%)",                      "roa",
  "MTM loss / TA (%)",            "ell",
  "E^MV / TA (%)",                "emv_ta",
  "v / TA (post-run, %)",         "v_postrun_ta",
  "% with E^MV < 0",              "emv_neg",              # treated as %
  "% with v < 0 (post-run)",      "v_postrun_neg"
)

build_two_panel_table <- function(get_ids_fn, type_label_1, type_label_2) {
  # Returns: list(table = data_frame, n_stat = number_of_variable_rows)

  build_panel <- function(type_label) {
    out <- tibble(Statistic = two_panel_vars$label)
    for (i in seq_len(nrow(col_specs))) {
      p <- col_specs$period[i]; f <- col_specs$facility[i]
      cid <- col_specs$col_id[i]
      ids <- get_ids_fn(p, f, type_label)
      cell <- df %>% filter(idrssd %in% ids)
      vals <- map_dbl(seq_len(nrow(two_panel_vars)), function(j) {
        col <- two_panel_vars$col[j]
        if (col == "N") return(nrow(cell))
        if (grepl("^%", two_panel_vars$label[j])) {
          return(100 * mean(cell[[col]], na.rm = TRUE))
        }
        mean(cell[[col]], na.rm = TRUE)
      })
      out[[cid]] <- vals
    }
    out
  }

  panelA <- build_panel(type_label_1)
  panelB <- build_panel(type_label_2)
  tbl <- bind_rows(panelA, panelB)
  list(table = tbl, n_stat = nrow(two_panel_vars))
}
# ══════════════════════════════════════════════════════════════════════
# Table A1 — New vs Returning borrowers
# Returning in period p = borrowed from DW or BTFP in any earlier period
# New in p = first-time appearance in DW or BTFP
# ══════════════════════════════════════════════════════════════════════

get_ids_new_ret <- function(p, f, t) {
  banks <- banks_in_cell(p, f)
  prior <- prior_set(p)
  if (t == "Returning") banks[banks %in% prior] else banks[!(banks %in% prior)]
}

out_newret <- build_two_panel_table(get_ids_new_ret, "New", "Returning")
tableA1    <- out_newret$table
n_stat_A1  <- out_newret$n_stat

cat(sprintf("Table A1 shape: %d x %d\n", nrow(tableA1), ncol(tableA1)))
## Table A1 shape: 32 x 7
tableA1 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 32 × 7
##    Statistic     `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>                         <dbl>               <dbl>                 <dbl>
##  1 N (banks)                    72                  32                  332     
##  2 Total assets…                 4.40               12.4                  6.77  
##  3 Cash / TA (%)                 4.38                3.31                 6.46  
##  4 Securities /…                26.7                21.1                 24.1   
##  5 Loans / TA (…                63.6                69.0                 63.4   
##  6 Book equity …                 9.05                8.70                 9.01  
##  7 Uninsured / …                32.0                36.2                 33.2   
##  8 Uninsured le…                26.9                29.8                 28.2   
##  9 Wholesale fu…                 5.65                6.22                 4.73  
## 10 FHLB / TA (%)                 3.53                4.69                 3.45  
## 11 ROA (%)                       1.03                1.18                 1.18  
## 12 MTM loss / T…                 9.33                9.07                 9.07  
## 13 E^MV / TA (%)                -0.274              -0.372               -0.0590
## 14 v / TA (post…                 9.28                8.63                 9.62  
## 15 % with E^MV …                59.7                53.1                 50.6   
## 16 % with v < 0…                 1.39                0                    4.26  
## 17 N (banks)                     0                  35                   65     
## 18 Total assets…               NaN                   3.20                 5.19  
## 19 Cash / TA (%)               NaN                   4.20                 4.13  
## 20 Securities /…               NaN                  28.5                 26.3   
## 21 Loans / TA (…               NaN                  62.0                 64.0   
## 22 Book equity …               NaN                   8.14                 8.32  
## 23 Uninsured / …               NaN                  35.0                 35.3   
## 24 Uninsured le…               NaN                  29.3                 29.6   
## 25 Wholesale fu…               NaN                   7.18                 6.62  
## 26 FHLB / TA (%)               NaN                   4.45                 4.44  
## 27 ROA (%)                     NaN                   0.915                1.03  
## 28 MTM loss / T…               NaN                   8.79                 8.83  
## 29 E^MV / TA (%)               NaN                  -0.653               -0.509 
## 30 v / TA (post…               NaN                   8.40                 8.47  
## 31 % with E^MV …               NaN                  62.9                 58.5   
## 32 % with v < 0…               NaN                   2.86                 1.54  
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <dbl>, `P3: FRC Week | DW` <dbl>,
## #   `P3: FRC Week | BTFP` <dbl>
# Print bank counts per cell (sanity vs Table 1's New/Returning rows)
cat("\nBank counts per cell (should reconcile to Table 1):\n")
## 
## Bank counts per cell (should reconcile to Table 1):
walk(seq_len(nrow(col_specs)), function(i) {
  p <- col_specs$period[i]; f <- col_specs$facility[i]
  n_new <- length(get_ids_new_ret(p, f, "New"))
  n_ret <- length(get_ids_new_ret(p, f, "Returning"))
  cat(sprintf("  %-22s  New = %3d  Returning = %3d  Total = %3d\n",
              col_specs$col_id[i], n_new, n_ret, n_new + n_ret))
})
##   P0: Pre-Crisis | DW     New =  72  Returning =   0  Total =  72
##   P1: SVB Week | DW       New =  33  Returning =  35  Total =  68
##   P2: SVB to FRC | DW     New = 340  Returning =  66  Total = 406
##   P2: SVB to FRC | BTFP   New = 442  Returning =  30  Total = 472
##   P3: FRC Week | DW       New =  34  Returning =  67  Total = 101
##   P3: FRC Week | BTFP     New =  54  Returning = 130  Total = 184
tableA1_k <- tableA1 %>%
  kbl(digits = 2, align = c("l", rep("r", 6)),
      caption = "Table A1: New vs Returning Borrowers by Period × Facility (2022Q4)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "left", font_size = 11) %>%
  add_header_above(c(" " = 1, "Pre-Crisis" = 1, "SVB Week" = 1,
                     "SVB → FRC" = 2, "FRC Week" = 2)) %>%
  pack_rows("Panel A. New borrowers",      1,           n_stat_A1) %>%
  pack_rows("Panel B. Returning borrowers", n_stat_A1+1, 2*n_stat_A1) %>%
  footnote(general = paste(
    "A bank is Returning in period p if it borrowed from either DW or BTFP",
    "in any earlier period; New otherwise. P0 has no prior period, so all P0",
    "borrowers are by construction New (Panel B for P0 is empty).",
    "Characteristics are 2022Q4 means (excluding GSIBs and failed banks).",
    "v = E_MV + F_insured (post uninsured-run fundamental value)."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
tableA1_k
Table A1: New vs Returning Borrowers by Period × Facility (2022Q4)
Pre-Crisis
SVB Week
SVB → FRC
FRC Week
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP
Panel A. New borrowers
N (banks) 72.00 32.00 332.00 431.00 34.00 53.00
Total assets (\(B) </td> <td style="text-align:right;"> 4.40 </td> <td style="text-align:right;"> 12.39 </td> <td style="text-align:right;"> 6.77 </td> <td style="text-align:right;"> 6.05 </td> <td style="text-align:right;"> 2.97 </td> <td style="text-align:right;"> 1.40 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Cash / TA (%) </td> <td style="text-align:right;"> 4.38 </td> <td style="text-align:right;"> 3.31 </td> <td style="text-align:right;"> 6.46 </td> <td style="text-align:right;"> 4.74 </td> <td style="text-align:right;"> 6.81 </td> <td style="text-align:right;"> 4.61 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Securities / TA (%) </td> <td style="text-align:right;"> 26.66 </td> <td style="text-align:right;"> 21.15 </td> <td style="text-align:right;"> 24.09 </td> <td style="text-align:right;"> 28.04 </td> <td style="text-align:right;"> 21.45 </td> <td style="text-align:right;"> 30.70 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Loans / TA (%) </td> <td style="text-align:right;"> 63.64 </td> <td style="text-align:right;"> 68.96 </td> <td style="text-align:right;"> 63.43 </td> <td style="text-align:right;"> 61.55 </td> <td style="text-align:right;"> 66.16 </td> <td style="text-align:right;"> 58.89 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Book equity / TA (%) </td> <td style="text-align:right;"> 9.05 </td> <td style="text-align:right;"> 8.70 </td> <td style="text-align:right;"> 9.01 </td> <td style="text-align:right;"> 8.13 </td> <td style="text-align:right;"> 9.68 </td> <td style="text-align:right;"> 8.31 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured / Deposits (%) </td> <td style="text-align:right;"> 31.99 </td> <td style="text-align:right;"> 36.15 </td> <td style="text-align:right;"> 33.16 </td> <td style="text-align:right;"> 31.66 </td> <td style="text-align:right;"> 26.58 </td> <td style="text-align:right;"> 29.21 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured leverage (%) </td> <td style="text-align:right;"> 26.87 </td> <td style="text-align:right;"> 29.80 </td> <td style="text-align:right;"> 28.23 </td> <td style="text-align:right;"> 27.12 </td> <td style="text-align:right;"> 22.86 </td> <td style="text-align:right;"> 25.54 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Wholesale funding / TA (%) </td> <td style="text-align:right;"> 5.65 </td> <td style="text-align:right;"> 6.22 </td> <td style="text-align:right;"> 4.73 </td> <td style="text-align:right;"> 5.38 </td> <td style="text-align:right;"> 3.28 </td> <td style="text-align:right;"> 3.61 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> FHLB / TA (%) </td> <td style="text-align:right;"> 3.53 </td> <td style="text-align:right;"> 4.69 </td> <td style="text-align:right;"> 3.45 </td> <td style="text-align:right;"> 4.01 </td> <td style="text-align:right;"> 2.25 </td> <td style="text-align:right;"> 2.38 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> ROA (%) </td> <td style="text-align:right;"> 1.03 </td> <td style="text-align:right;"> 1.18 </td> <td style="text-align:right;"> 1.18 </td> <td style="text-align:right;"> 1.07 </td> <td style="text-align:right;"> 1.12 </td> <td style="text-align:right;"> 1.05 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> MTM loss / TA (%) </td> <td style="text-align:right;"> 9.33 </td> <td style="text-align:right;"> 9.07 </td> <td style="text-align:right;"> 9.07 </td> <td style="text-align:right;"> 9.67 </td> <td style="text-align:right;"> 8.06 </td> <td style="text-align:right;"> 9.35 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> E^MV / TA (%) </td> <td style="text-align:right;"> -0.27 </td> <td style="text-align:right;"> -0.37 </td> <td style="text-align:right;"> -0.06 </td> <td style="text-align:right;"> -1.54 </td> <td style="text-align:right;"> 1.62 </td> <td style="text-align:right;"> -1.05 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> v / TA (post-run, %) </td> <td style="text-align:right;"> 9.28 </td> <td style="text-align:right;"> 8.63 </td> <td style="text-align:right;"> 9.62 </td> <td style="text-align:right;"> 8.43 </td> <td style="text-align:right;"> 12.14 </td> <td style="text-align:right;"> 9.62 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> % with E^MV &lt; 0 </td> <td style="text-align:right;"> 59.72 </td> <td style="text-align:right;"> 53.12 </td> <td style="text-align:right;"> 50.60 </td> <td style="text-align:right;"> 59.16 </td> <td style="text-align:right;"> 35.29 </td> <td style="text-align:right;"> 52.83 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> % with v &lt; 0 (post-run) </td> <td style="text-align:right;"> 1.39 </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 4.26 </td> <td style="text-align:right;"> 4.46 </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 1.89 </td> </tr> <tr grouplength="16"><td colspan="7" style="border-bottom: 1px solid;"><strong>Panel B. Returning borrowers</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> N (banks) </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 35.00 </td> <td style="text-align:right;"> 65.00 </td> <td style="text-align:right;"> 29.00 </td> <td style="text-align:right;"> 67.00 </td> <td style="text-align:right;"> 129.00 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total assets (\)B) NaN 3.20 5.19 10.74 2.66 6.65
Cash / TA (%) NaN 4.20 4.13 3.87 4.60 4.02
Securities / TA (%) NaN 28.46 26.32 27.78 26.83 29.44
Loans / TA (%) NaN 62.02 64.00 62.55 63.13 60.96
Book equity / TA (%) NaN 8.14 8.32 8.58 8.64 8.01
Uninsured / Deposits (%) NaN 35.00 35.33 38.59 32.87 31.92
Uninsured leverage (%) NaN 29.27 29.58 31.86 27.53 27.44
Wholesale funding / TA (%) NaN 7.18 6.62 6.96 6.37 5.70
FHLB / TA (%) NaN 4.45 4.44 4.44 4.47 4.08
ROA (%) NaN 0.92 1.03 1.09 1.16 1.00
MTM loss / TA (%) NaN 8.79 8.83 8.62 9.14 10.13
E^MV / TA (%) NaN -0.65 -0.51 -0.04 -0.50 -2.12
v / TA (post-run, %) NaN 8.40 8.47 8.37 9.08 7.83
% with E^MV < 0 NaN 62.86 58.46 55.17 55.22 62.79
% with v < 0 (post-run) NaN 2.86 1.54 3.45 3.03 6.98
Notes: A bank is Returning in period p if it borrowed from either DW or BTFP in any earlier period; New otherwise. P0 has no prior period, so all P0 borrowers are by construction New (Panel B for P0 is empty). Characteristics are 2022Q4 means (excluding GSIBs and failed banks). v = E_MV + F_insured (post uninsured-run fundamental value).
save_kbl_latex(tableA1, "tableA1_new_vs_returning",
               caption = "New vs Returning Borrowers by Period × Facility")

Table 6B — One-time vs Repeat borrowers

# ══════════════════════════════════════════════════════════════════════
# Table A2 — One-time vs Repeat borrowers
# One-time: 1 distinct loan date in the cell
# Repeat  : ≥2 distinct loan dates in the cell
# ══════════════════════════════════════════════════════════════════════

dw_cell_types <- dw_pf %>%
  distinct(period, rssd_id, dw_loan_date) %>%
  count(period, rssd_id, name = "n_dates") %>%
  mutate(type = if_else(n_dates > 1, "Repeat", "One-time"),
         facility = "DW")

btfp_cell_types <- btfp_pf %>%
  distinct(period, rssd_id, btfp_loan_date) %>%
  count(period, rssd_id, name = "n_dates") %>%
  mutate(type = if_else(n_dates > 1, "Repeat", "One-time"),
         facility = "BTFP")

cell_types <- bind_rows(dw_cell_types, btfp_cell_types)

get_ids_onetime_rep <- function(p, f, t) {
  cell_types %>%
    filter(period == p, facility == f, type == t) %>%
    pull(rssd_id)
}

out_otrep <- build_two_panel_table(get_ids_onetime_rep, "One-time", "Repeat")
tableA2   <- out_otrep$table
n_stat_A2 <- out_otrep$n_stat

cat(sprintf("Table A2 shape: %d x %d\n", nrow(tableA2), ncol(tableA2)))
## Table A2 shape: 32 x 7
tableA2 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 32 × 7
##    Statistic     `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>                         <dbl>               <dbl>                 <dbl>
##  1 N (banks)                    40                 37                    284    
##  2 Total assets…                 5.27              11.0                    7.57 
##  3 Cash / TA (%)                 4.74               4.52                   6.84 
##  4 Securities /…                23.5               22.1                   23.2  
##  5 Loans / TA (…                66.2               67.2                   64.1  
##  6 Book equity …                 9.81               8.75                   9.20 
##  7 Uninsured / …                28.5               36.7                   32.4  
##  8 Uninsured le…                23.8               30.5                   27.7  
##  9 Wholesale fu…                 5.22               5.74                   4.41 
## 10 FHLB / TA (%)                 3.49               4.01                   3.32 
## 11 ROA (%)                       1.06               1.10                   1.19 
## 12 MTM loss / T…                 9.16               8.66                   8.87 
## 13 E^MV / TA (%)                 0.657              0.0895                 0.329
## 14 v / TA (post…                10.5                8.98                   9.99 
## 15 % with E^MV …                50                 48.6                   47.9  
## 16 % with v < 0…                 0                  0                      3.18 
## 17 N (banks)                    32                 30                    113    
## 18 Total assets…                 3.31               3.39                   3.87 
## 19 Cash / TA (%)                 3.92               2.85                   4.16 
## 20 Securities /…                30.6               28.5                   27.7  
## 21 Loans / TA (…                60.4               63.0                   62.1  
## 22 Book equity …                 8.10               7.98                   8.13 
## 23 Uninsured / …                36.4               34.2                   36.2  
## 24 Uninsured le…                30.7               28.3                   30.3  
## 25 Wholesale fu…                 6.18               7.94                   6.61 
## 26 FHLB / TA (%)                 3.57               5.25                   4.35 
## 27 ROA (%)                       0.989              0.964                  1.08 
## 28 MTM loss / T…                 9.54               9.25                   9.42 
## 29 E^MV / TA (%)                -1.44              -1.27                  -1.29 
## 30 v / TA (post…                 7.73               7.93                   8.01 
## 31 % with E^MV …                71.9               70                     61.9  
## 32 % with v < 0…                 3.12               3.33                   5.41 
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <dbl>, `P3: FRC Week | DW` <dbl>,
## #   `P3: FRC Week | BTFP` <dbl>
cat("\nBank counts per cell (one-time | repeat):\n")
## 
## Bank counts per cell (one-time | repeat):
walk(seq_len(nrow(col_specs)), function(i) {
  p <- col_specs$period[i]; f <- col_specs$facility[i]
  n_ot <- length(get_ids_onetime_rep(p, f, "One-time"))
  n_rp <- length(get_ids_onetime_rep(p, f, "Repeat"))
  cat(sprintf("  %-22s  One-time = %3d  Repeat = %3d  Total = %3d\n",
              col_specs$col_id[i], n_ot, n_rp, n_ot + n_rp))
})
##   P0: Pre-Crisis | DW     One-time =  40  Repeat =  32  Total =  72
##   P1: SVB Week | DW       One-time =  37  Repeat =  31  Total =  68
##   P2: SVB to FRC | DW     One-time = 292  Repeat = 114  Total = 406
##   P2: SVB to FRC | BTFP   One-time = 235  Repeat = 237  Total = 472
##   P3: FRC Week | DW       One-time =  56  Repeat =  45  Total = 101
##   P3: FRC Week | BTFP     One-time = 126  Repeat =  58  Total = 184
tableA2_k <- tableA2 %>%
  kbl(digits = 2, align = c("l", rep("r", 6)),
      caption = "Table A2: One-time vs Repeat Borrowers by Period × Facility (2022Q4)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "left", font_size = 11) %>%
  add_header_above(c(" " = 1, "Pre-Crisis" = 1, "SVB Week" = 1,
                     "SVB → FRC" = 2, "FRC Week" = 2)) %>%
  pack_rows("Panel A. One-time borrowers (1 distinct loan date)",
            1,           n_stat_A2) %>%
  pack_rows("Panel B. Repeat borrowers (≥2 distinct loan dates)",
            n_stat_A2+1, 2*n_stat_A2) %>%
  footnote(general = paste(
    "One-time vs Repeat is defined within each period × facility cell by the",
    "number of distinct loan dates during that cell's date range. A bank can",
    "appear as One-time in one cell and Repeat in another. Characteristics are",
    "2022Q4 means (excluding GSIBs and failed banks)."),
    general_title = "Notes:", footnote_as_chunk = TRUE)
tableA2_k
Table A2: One-time vs Repeat Borrowers by Period × Facility (2022Q4)
Pre-Crisis
SVB Week
SVB → FRC
FRC Week
Statistic P0: Pre-Crisis | DW P1: SVB Week | DW P2: SVB to FRC | DW P2: SVB to FRC | BTFP P3: FRC Week | DW P3: FRC Week | BTFP
Panel A. One-time borrowers (1 distinct loan date)
N (banks) 40.00 37.00 284.00 229.00 56.00 124.00
Total assets (\(B) </td> <td style="text-align:right;"> 5.27 </td> <td style="text-align:right;"> 11.00 </td> <td style="text-align:right;"> 7.57 </td> <td style="text-align:right;"> 9.25 </td> <td style="text-align:right;"> 2.56 </td> <td style="text-align:right;"> 6.13 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Cash / TA (%) </td> <td style="text-align:right;"> 4.74 </td> <td style="text-align:right;"> 4.52 </td> <td style="text-align:right;"> 6.84 </td> <td style="text-align:right;"> 5.08 </td> <td style="text-align:right;"> 6.62 </td> <td style="text-align:right;"> 4.09 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Securities / TA (%) </td> <td style="text-align:right;"> 23.53 </td> <td style="text-align:right;"> 22.07 </td> <td style="text-align:right;"> 23.17 </td> <td style="text-align:right;"> 26.45 </td> <td style="text-align:right;"> 21.93 </td> <td style="text-align:right;"> 29.91 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Loans / TA (%) </td> <td style="text-align:right;"> 66.22 </td> <td style="text-align:right;"> 67.20 </td> <td style="text-align:right;"> 64.08 </td> <td style="text-align:right;"> 62.93 </td> <td style="text-align:right;"> 65.80 </td> <td style="text-align:right;"> 60.23 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Book equity / TA (%) </td> <td style="text-align:right;"> 9.81 </td> <td style="text-align:right;"> 8.75 </td> <td style="text-align:right;"> 9.20 </td> <td style="text-align:right;"> 8.11 </td> <td style="text-align:right;"> 9.41 </td> <td style="text-align:right;"> 7.97 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured / Deposits (%) </td> <td style="text-align:right;"> 28.49 </td> <td style="text-align:right;"> 36.67 </td> <td style="text-align:right;"> 32.45 </td> <td style="text-align:right;"> 32.87 </td> <td style="text-align:right;"> 28.23 </td> <td style="text-align:right;"> 30.02 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Uninsured leverage (%) </td> <td style="text-align:right;"> 23.79 </td> <td style="text-align:right;"> 30.50 </td> <td style="text-align:right;"> 27.70 </td> <td style="text-align:right;"> 28.24 </td> <td style="text-align:right;"> 23.92 </td> <td style="text-align:right;"> 26.00 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Wholesale funding / TA (%) </td> <td style="text-align:right;"> 5.22 </td> <td style="text-align:right;"> 5.74 </td> <td style="text-align:right;"> 4.41 </td> <td style="text-align:right;"> 4.89 </td> <td style="text-align:right;"> 4.51 </td> <td style="text-align:right;"> 5.06 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> FHLB / TA (%) </td> <td style="text-align:right;"> 3.49 </td> <td style="text-align:right;"> 4.01 </td> <td style="text-align:right;"> 3.32 </td> <td style="text-align:right;"> 3.71 </td> <td style="text-align:right;"> 3.25 </td> <td style="text-align:right;"> 3.58 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> ROA (%) </td> <td style="text-align:right;"> 1.06 </td> <td style="text-align:right;"> 1.10 </td> <td style="text-align:right;"> 1.19 </td> <td style="text-align:right;"> 1.08 </td> <td style="text-align:right;"> 1.25 </td> <td style="text-align:right;"> 1.00 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> MTM loss / TA (%) </td> <td style="text-align:right;"> 9.16 </td> <td style="text-align:right;"> 8.66 </td> <td style="text-align:right;"> 8.87 </td> <td style="text-align:right;"> 9.28 </td> <td style="text-align:right;"> 8.54 </td> <td style="text-align:right;"> 9.94 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> E^MV / TA (%) </td> <td style="text-align:right;"> 0.66 </td> <td style="text-align:right;"> 0.09 </td> <td style="text-align:right;"> 0.33 </td> <td style="text-align:right;"> -1.16 </td> <td style="text-align:right;"> 0.87 </td> <td style="text-align:right;"> -1.97 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> v / TA (post-run, %) </td> <td style="text-align:right;"> 10.53 </td> <td style="text-align:right;"> 8.98 </td> <td style="text-align:right;"> 9.99 </td> <td style="text-align:right;"> 8.78 </td> <td style="text-align:right;"> 11.04 </td> <td style="text-align:right;"> 8.36 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> % with E^MV &lt; 0 </td> <td style="text-align:right;"> 50.00 </td> <td style="text-align:right;"> 48.65 </td> <td style="text-align:right;"> 47.89 </td> <td style="text-align:right;"> 57.64 </td> <td style="text-align:right;"> 44.64 </td> <td style="text-align:right;"> 62.10 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> % with v &lt; 0 (post-run) </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 3.18 </td> <td style="text-align:right;"> 1.77 </td> <td style="text-align:right;"> 0.00 </td> <td style="text-align:right;"> 6.45 </td> </tr> <tr grouplength="16"><td colspan="7" style="border-bottom: 1px solid;"><strong>Panel B. Repeat borrowers (≥2 distinct loan dates)</strong></td></tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> N (banks) </td> <td style="text-align:right;"> 32.00 </td> <td style="text-align:right;"> 30.00 </td> <td style="text-align:right;"> 113.00 </td> <td style="text-align:right;"> 231.00 </td> <td style="text-align:right;"> 45.00 </td> <td style="text-align:right;"> 58.00 </td> </tr> <tr> <td style="text-align:left;padding-left: 2em;" indentlevel="1"> Total assets (\)B) 3.31 3.39 3.87 3.48 3.02 2.95
Cash / TA (%) 3.92 2.85 4.16 4.29 3.76 4.41
Securities / TA (%) 30.56 28.55 27.69 29.59 28.87 29.57
Loans / TA (%) 60.42 63.02 62.14 60.31 62.09 60.63
Book equity / TA (%) 8.10 7.98 8.13 8.21 8.47 8.37
Uninsured / Deposits (%) 36.36 34.17 36.19 31.33 33.89 33.50
Uninsured leverage (%) 30.72 28.32 30.35 26.61 28.49 28.78
Wholesale funding / TA (%) 6.18 7.94 6.61 6.05 6.35 5.16
FHLB / TA (%) 3.57 5.25 4.35 4.36 4.30 3.58
ROA (%) 0.99 0.96 1.08 1.06 1.02 1.04
MTM loss / TA (%) 9.54 9.25 9.42 9.94 9.07 9.83
E^MV / TA (%) -1.44 -1.27 -1.29 -1.73 -0.60 -1.46
v / TA (post-run, %) 7.73 7.93 8.01 8.08 8.95 8.33
% with E^MV < 0 71.88 70.00 61.95 60.17 53.33 55.17
% with v < 0 (post-run) 3.12 3.33 5.41 6.99 4.55 3.45
Notes: One-time vs Repeat is defined within each period × facility cell by the number of distinct loan dates during that cell’s date range. A bank can appear as One-time in one cell and Repeat in another. Characteristics are 2022Q4 means (excluding GSIBs and failed banks).
save_kbl_latex(tableA2, "tableA2_onetime_vs_repeat",
               caption = "One-time vs Repeat Borrowers by Period × Facility")

5. Regressions

All regressions use the fit_model() wrapper. To switch from LPM to logit or probit for the extensive-margin regressions, change MODEL_FAMILY at the top of the document from "lpm" to "logit" or "probit". All extensive-margin results will re-estimate.

5.1 Variable specs shared across regressions

# Right-hand side of the baseline extensive-margin regression
rhs_base <- "ell_w_z * du_ta_w_z + log_ta_w_z + cash_ta_w_z + loan_to_dep_w_z + eq_ta_w_z + wholesale_ta_w_z + roa_w_z"

# Variable labels for printing
var_order <- c("ell_w_z", "du_ta_w_z", "ell_w_z:du_ta_w_z",
               "log_ta_w_z", "cash_ta_w_z", "loan_to_dep_w_z",
               "eq_ta_w_z", "wholesale_ta_w_z", "roa_w_z",
               "(Intercept)")

var_labels <- c(
  "ell_w_z"             = "MTM loss (total)",
  "du_ta_w_z"           = "Uninsured leverage",
  "ell_w_z:du_ta_w_z"   = "MTM × Uninsured Lev",
  "log_ta_w_z"          = "Ln(assets)",
  "cash_ta_w_z"         = "Cash / TA",
  "loan_to_dep_w_z"     = "Loan / Deposit",
  "eq_ta_w_z"           = "Book equity / TA",
  "wholesale_ta_w_z"    = "Wholesale funding",
  "roa_w_z"             = "ROA",
  "(Intercept)"         = "Constant"
)

#' Build a coefficient table from a list of models
#' @param models Named list of fitted models
#' @param var_order Character vector of term names in print order
#' @param var_labels Named character vector mapping term → label
build_coef_table <- function(models, var_order, var_labels) {
  tidies <- lapply(models, get_robust_tidy)
  body <- list()
  for (v in var_order) {
    est_row <- c(var_labels[[v]])
    se_row  <- c("")
    for (tm in tidies) {
      idx <- which(tm$term == v)
      if (length(idx) == 0) {
        est_row <- c(est_row, ""); se_row <- c(se_row, "")
      } else {
        est_row <- c(est_row, fmt_est(tm$estimate[idx], tm$p.value[idx]))
        se_row  <- c(se_row,  fmt_se(tm$std.error[idx]))
      }
    }
    body[[paste0(v, "_est")]] <- est_row
    body[[paste0(v, "_se")]]  <- se_row
  }
  # Footer rows
  n_row    <- c("N")
  fit_row  <- c(NA_character_)
  mdv_row  <- c("Mean of DV")
  fit_lbl_set <- FALSE
  for (m in models) {
    fs <- get_fit_stats(m)
    if (!fit_lbl_set) { fit_row[1] <- fs$fit_label; fit_lbl_set <- TRUE }
    n_row    <- c(n_row,   fmt(fs$n))
    fit_row  <- c(fit_row, sprintf("%.3f", fs$fit_value))
    mdv_row  <- c(mdv_row, sprintf("%.4f", mean(m$model[[1]], na.rm = TRUE)))
  }
  body[["_blank"]] <- rep("", length(models) + 1)
  body[["_N"]]     <- n_row
  body[["_fit"]]   <- fit_row
  body[["_mdv"]]   <- mdv_row

  tab <- do.call(rbind, body) %>% as.data.frame(stringsAsFactors = FALSE)
  rownames(tab) <- NULL
  colnames(tab) <- c("Variable", names(models))
  tab
}

5.2 Table 6 — Main LPM (extensive margin, all cells)

# Build DVs: one for each (period, facility) cell on the full cross-section
build_dep <- function(df, cell_spec) {
  ids <- banks_in_cell(cell_spec$period, cell_spec$facility)
  as.integer(df$idrssd %in% ids)
}

df_reg <- df %>% filter(!is.na(du_ta_w_z), !is.na(ell_w_z))

dep_vars <- list()
for (i in seq_len(nrow(col_specs))) {
  cs <- col_specs[i, ]
  dep_name <- paste0("dep_",
                     gsub("[^a-z0-9]", "_", tolower(cs$col_id)))
  df_reg[[dep_name]] <- build_dep(df_reg, cs)
  dep_vars[[cs$col_id]] <- dep_name
}

# Fit one regression per cell
models_T6 <- list()
for (nm in names(dep_vars)) {
  f <- as.formula(paste(dep_vars[[nm]], "~", rhs_base))
  models_T6[[nm]] <- fit_model(f, df_reg)
}

table6 <- build_coef_table(models_T6, var_order, var_labels)
table6 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 "MTM loss (t… "0.001"               "-0.002"            "0.012**"            
##  2 ""            "(0.002)"             "(0.002)"           "(0.005)"            
##  3 "Uninsured l… "0.000"               "0.002"             "0.012**"            
##  4 ""            "(0.003)"             "(0.002)"           "(0.005)"            
##  5 "MTM × Unins… "0.002"               "0.001"             "0.013***"           
##  6 ""            "(0.002)"             "(0.002)"           "(0.004)"            
##  7 "Ln(assets)"  "0.013***"            "0.014***"          "0.066***"           
##  8 ""            "(0.003)"             "(0.003)"           "(0.006)"            
##  9 "Cash / TA"   "-0.004*"             "-0.003**"          "0.003"              
## 10 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 11 "Loan / Depo… "-0.005*"             "-0.005**"          "-0.007"             
## 12 ""            "(0.003)"             "(0.002)"           "(0.005)"            
## 13 "Book equity… "0.002"               "-0.000"            "0.001"              
## 14 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 15 "Wholesale f… "0.006**"             "0.008***"          "0.015***"           
## 16 ""            "(0.003)"             "(0.003)"           "(0.005)"            
## 17 "ROA"         "-0.002"              "-0.002"            "0.001"              
## 18 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 19 "Constant"    "0.016***"            "0.014***"          "0.086***"           
## 20 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 21 ""            ""                    ""                  ""                   
## 22 "N"           "4,632"               "4,632"             "4,632"              
## 23 "R²"          "0.015"               "0.023"             "0.069"              
## 24 "Mean of DV"  "0.0155"              "0.0145"            "0.0857"             
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(table6, sprintf("table6_main_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Main extensive-margin regression (%s) across sub-period × facility cells.",
                 toupper(MODEL_FAMILY)))

5.3 Table 7 — Robustness: drop top-10 borrowers by $

Re-run Table 6 excluding the top 10 of borrowers by cumulative $ to check whether the interaction mechanism survives the removal of volume drivers.

# ══════════════════════════════════════════════════════════════════════
# Table 7 — Robustness: drop the TOP 10 BANKS by cumulative crisis $
# ══════════════════════════════════════════════════════════════════════

# ── Option 2 (primary): 10 largest crisis-wide borrowers ───────────
top10_ids <- df_reg %>%
  filter(borrowed == 1) %>%
  arrange(desc(total_borrow)) %>%
  slice_head(n = 10) %>%
  pull(idrssd)

df_reg_trim <- df_reg %>% filter(!(idrssd %in% top10_ids))

cat(sprintf("Dropping %d banks (the 10 largest crisis-wide borrowers):\n",
            length(top10_ids)))
## Dropping 10 banks (the 10 largest crisis-wide borrowers):
df_reg %>%
  filter(idrssd %in% top10_ids) %>%
  arrange(desc(total_borrow)) %>%
  transmute(Rank = row_number(),
            RSSD = idrssd,
            `Cum $ (M)` = round(total_borrow * 1000, 0),
            `TA ($B)`   = round(total_asset / 1e6, 2)) %>%
  print()
## # A tibble: 10 × 4
##     Rank RSSD     `Cum $ (M)` `TA ($B)`
##    <int> <chr>          <dbl>     <dbl>
##  1     1 3138146 269300000000     67.7 
##  2     2 494261  144715000000     41.2 
##  3     3 3284397  39050000000     26.0 
##  4     4 671464   27163000000     19.2 
##  5     5 936855   22525000000     38.3 
##  6     6 936462   18263000000     18.1 
##  7     7 450856   10919500000      7.61
##  8     8 197478   10000100000     64.1 
##  9     9 200378    8760000000      9.19
## 10    10 739560    7080000000      5.45
# ── Fit regressions on trimmed sample ──────────────────────────────
models_T7 <- list()
for (nm in names(dep_vars)) {
  f <- as.formula(paste(dep_vars[[nm]], "~", rhs_base))
  models_T7[[nm]] <- fit_model(f, df_reg_trim)
}

table7 <- build_coef_table(models_T7, var_order, var_labels)
table7 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 "MTM loss (t… "0.000"               "-0.003"            "0.011**"            
##  2 ""            "(0.002)"             "(0.002)"           "(0.005)"            
##  3 "Uninsured l… "-0.000"              "0.000"             "0.010*"             
##  4 ""            "(0.002)"             "(0.002)"           "(0.005)"            
##  5 "MTM × Unins… "0.001"               "-0.001"            "0.011***"           
##  6 ""            "(0.002)"             "(0.002)"           "(0.004)"            
##  7 "Ln(assets)"  "0.012***"            "0.012***"          "0.063***"           
##  8 ""            "(0.003)"             "(0.003)"           "(0.006)"            
##  9 "Cash / TA"   "-0.004*"             "-0.004**"          "0.003"              
## 10 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 11 "Loan / Depo… "-0.005*"             "-0.005**"          "-0.007"             
## 12 ""            "(0.003)"             "(0.002)"           "(0.005)"            
## 13 "Book equity… "0.002"               "-0.001"            "-0.000"             
## 14 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 15 "Wholesale f… "0.006**"             "0.008***"          "0.015***"           
## 16 ""            "(0.003)"             "(0.003)"           "(0.005)"            
## 17 "ROA"         "-0.002"              "-0.002"            "0.002"              
## 18 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 19 "Constant"    "0.015***"            "0.013***"          "0.085***"           
## 20 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 21 ""            ""                    ""                  ""                   
## 22 "N"           "4,622"               "4,622"             "4,622"              
## 23 "R²"          "0.012"               "0.018"             "0.063"              
## 24 "Mean of DV"  "0.0147"              "0.0132"            "0.0839"             
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(table7, sprintf("table7_drop_top10_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Robustness (%s): main regression excluding the 10 largest crisis-wide borrowers.",
                 toupper(MODEL_FAMILY)))

5.4 Table 8 — Robustness: drop BTFP pure testers

tester_ids <- btfp_bank_summary %>%
  filter(tester_type == "Pure tester") %>% pull(rssd_id)

df_reg_notest <- df_reg %>% filter(!(idrssd %in% tester_ids))
cat(sprintf("Excluding %d BTFP pure testers from the sample.\n",
            length(tester_ids)))
## Excluding 46 BTFP pure testers from the sample.
models_T8 <- list()
for (nm in names(dep_vars)) {
  f <- as.formula(paste(dep_vars[[nm]], "~", rhs_base))
  models_T8[[nm]] <- fit_model(f, df_reg_notest)
}

table8 <- build_coef_table(models_T8, var_order, var_labels)

table8 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 "MTM loss (t… "0.001"               "-0.001"            "0.012**"            
##  2 ""            "(0.003)"             "(0.002)"           "(0.005)"            
##  3 "Uninsured l… "0.000"               "0.001"             "0.010*"             
##  4 ""            "(0.003)"             "(0.002)"           "(0.005)"            
##  5 "MTM × Unins… "0.002"               "0.001"             "0.012***"           
##  6 ""            "(0.002)"             "(0.002)"           "(0.004)"            
##  7 "Ln(assets)"  "0.014***"            "0.014***"          "0.063***"           
##  8 ""            "(0.003)"             "(0.003)"           "(0.006)"            
##  9 "Cash / TA"   "-0.004*"             "-0.003**"          "0.002"              
## 10 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 11 "Loan / Depo… "-0.005*"             "-0.005**"          "-0.006"             
## 12 ""            "(0.003)"             "(0.002)"           "(0.005)"            
## 13 "Book equity… "0.002"               "-0.000"            "0.001"              
## 14 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 15 "Wholesale f… "0.006**"             "0.008***"          "0.015***"           
## 16 ""            "(0.003)"             "(0.003)"           "(0.005)"            
## 17 "ROA"         "-0.002"              "-0.002"            "0.002"              
## 18 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 19 "Constant"    "0.016***"            "0.014***"          "0.083***"           
## 20 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 21 ""            ""                    ""                  ""                   
## 22 "N"           "4,589"               "4,589"             "4,589"              
## 23 "R²"          "0.015"               "0.021"             "0.066"              
## 24 "Mean of DV"  "0.0157"              "0.0142"            "0.0822"             
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(table8, sprintf("table8_drop_testers_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Robustness (%s): drop BTFP pure testers.",
                 toupper(MODEL_FAMILY)))

5.5 Table 9 — Two-regime analysis (ordinary vs extreme borrowers)

Conditional on borrowing, compare ordinary (bottom 90%) vs extreme (top 10%) borrowers. Uses log(total_borrow + 1) as DV because this is intensive-margin. The two-regime table always uses lm() regardless of MODEL_FAMILY, since the DV is continuous.

borrowers <- df_reg %>% filter(borrowed == 1) %>%
  mutate(cum_m = total_borrow * 1000)   # thousands → $M

ext_cutoff <- quantile(borrowers$cum_m, 0.90, na.rm = TRUE)
borrowers  <- borrowers %>%
  mutate(extreme = as.integer(cum_m >= ext_cutoff))

ord_df <- borrowers %>% filter(extreme == 0)
ext_df <- borrowers %>% filter(extreme == 1)

m_full <- lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = borrowers)
m_ord  <- lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = ord_df)
m_ext  <- lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = ext_df)

chow <- chow_test(rhs_base, "log(cum_m)", borrowers, "extreme")

cat(sprintf("Chow test: H0 all slopes equal across ordinary vs extreme.\n"))
## Chow test: H0 all slopes equal across ordinary vs extreme.
cat(sprintf("  F(%d,%d) = %.3f, p = %.4f\n",
            9, nrow(borrowers) - 20, chow$chi2, chow$p_value))
##   F(9,840) = 16.304, p = 0.0000
models_T9 <- list(Full = m_full, Ordinary = m_ord, Extreme = m_ext)
table9 <- build_coef_table(models_T9, var_order, var_labels)


table9 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 4
##    Variable              Full        Ordinary    Extreme    
##    <chr>                 <chr>       <chr>       <chr>      
##  1 "MTM loss (total)"    "-0.248"    "-0.229"    "-0.127"   
##  2 ""                    "(0.186)"   "(0.185)"   "(0.168)"  
##  3 "Uninsured leverage"  "0.661***"  "0.758***"  "0.111"    
##  4 ""                    "(0.178)"   "(0.163)"   "(0.124)"  
##  5 "MTM × Uninsured Lev" "-0.046"    "-0.276**"  "0.273**"  
##  6 ""                    "(0.146)"   "(0.139)"   "(0.113)"  
##  7 "Ln(assets)"          "0.332*"    "-0.727***" "0.845***" 
##  8 ""                    "(0.191)"   "(0.179)"   "(0.169)"  
##  9 "Cash / TA"           "-1.909***" "-2.149***" "0.169"    
## 10 ""                    "(0.271)"   "(0.236)"   "(0.173)"  
## 11 "Loan / Deposit"      "-0.932***" "-0.615***" "0.038"    
## 12 ""                    "(0.240)"   "(0.230)"   "(0.246)"  
## 13 "Book equity / TA"    "-0.157"    "-0.248"    "0.044"    
## 14 ""                    "(0.257)"   "(0.252)"   "(0.250)"  
## 15 "Wholesale funding"   "0.912***"  "0.640***"  "0.029"    
## 16 ""                    "(0.150)"   "(0.144)"   "(0.114)"  
## 17 "ROA"                 "0.043"     "0.037"     "0.178"    
## 18 ""                    "(0.180)"   "(0.176)"   "(0.160)"  
## 19 "Constant"            "13.500***" "13.375***" "19.260***"
## 20 ""                    "(0.185)"   "(0.179)"   "(0.351)"  
## 21 ""                    ""          ""          ""         
## 22 "N"                   "860"       "774"       "86"       
## 23 "R²"                  "0.152"     "0.143"     "0.323"    
## 24 "Mean of DV"          "14.7768"   "14.0938"   "20.9237"
save_kbl_latex(table9, "table9_two_regime",
               caption = sprintf(
                 "Two-regime analysis: log(cum $) regression on full borrower sample, ordinary (bottom 90%%), and extreme (top 10%%). Chow test p = %.4f.",
                 chow$p_value))

5.6 Table 10 — Extreme-borrower analysis by facility

run_facility_regime <- function(fac_grp_data, label) {
  n <- nrow(fac_grp_data)
  if (n < 50) {
    cat(sprintf("%s: sample too small (N=%d); skipping.\n", label, n))
    return(NULL)
  }
  cutoff <- quantile(fac_grp_data$cum_m, 0.90, na.rm = TRUE)
  fac_grp_data <- fac_grp_data %>% mutate(extreme = as.integer(cum_m >= cutoff))
  ord <- fac_grp_data %>% filter(extreme == 0)
  ext <- fac_grp_data %>% filter(extreme == 1)

  if (nrow(ext) < 15) {
    cat(sprintf("%s: fewer than 15 extreme banks (N_ext=%d); unstable.\n",
                label, nrow(ext)))
  }
  list(
    full = lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = fac_grp_data),
    ord  = lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = ord),
    ext  = lm(as.formula(paste("log(cum_m) ~", rhs_base)), data = ext),
    chow = tryCatch(chow_test(rhs_base, "log(cum_m)", fac_grp_data, "extreme"),
                    error = function(e) tibble(chi2 = NA, p_value = NA))
  )
}

dw_only   <- borrowers %>% filter(used_dw == 1, used_btfp == 0)
btfp_only <- borrowers %>% filter(used_btfp == 1, used_dw == 0)
both      <- borrowers %>% filter(used_dw == 1, used_btfp == 1)

r_dw   <- run_facility_regime(dw_only,   "DW Only")
r_btfp <- run_facility_regime(btfp_only, "BTFP Only")
r_both <- run_facility_regime(both,      "Both")
## Both: fewer than 15 extreme banks (N_ext=11); unstable.
cat("\nChow test p-values by facility:\n")
## 
## Chow test p-values by facility:
cat(sprintf("  DW Only  : p = %.4f\n", r_dw$chow$p_value))
##   DW Only  : p = 0.0000
cat(sprintf("  BTFP Only: p = %.4f\n", r_btfp$chow$p_value))
##   BTFP Only: p = 0.0335
cat(sprintf("  Both     : p = %.4f\n", r_both$chow$p_value))
##   Both     : p = 0.0000
# Build compact cross-facility summary
summarise_facility <- function(r, lbl) {
  get_coef <- function(m, v) {
    if (is.null(m)) return("")
    tm <- get_robust_tidy(m)
    i <- which(tm$term == v)
    if (length(i) == 0) "" else fmt_est(tm$estimate[i], tm$p.value[i])
  }
  tibble(
    Facility        = lbl,
    `Chow p`        = sprintf("%.4f", r$chow$p_value),
    `MTM×U (ord)`   = get_coef(r$ord, "ell_w_z:du_ta_w_z"),
    `MTM×U (ext)`   = get_coef(r$ext, "ell_w_z:du_ta_w_z"),
    `Ln(TA) (ord)`  = get_coef(r$ord, "log_ta_w_z"),
    `Ln(TA) (ext)`  = get_coef(r$ext, "log_ta_w_z"),
    `Cash/TA (ord)` = get_coef(r$ord, "cash_ta_w_z"),
    `Cash/TA (ext)` = get_coef(r$ext, "cash_ta_w_z"),
    `N (ord)`       = if (is.null(r$ord)) "" else fmt(nobs(r$ord)),
    `N (ext)`       = if (is.null(r$ext)) "" else fmt(nobs(r$ext))
  )
}

table10 <- bind_rows(
  summarise_facility(r_dw,   "DW Only"),
  summarise_facility(r_btfp, "BTFP Only"),
  summarise_facility(r_both, "Both")
)

table10 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 3 × 10
##   Facility  `Chow p` `MTM×U (ord)` `MTM×U (ext)` `Ln(TA) (ord)` `Ln(TA) (ext)`
##   <chr>     <chr>    <chr>         <chr>         <chr>          <chr>         
## 1 DW Only   0.0000   0.089         -0.083        -0.495*        0.480         
## 2 BTFP Only 0.0335   -0.107        0.265***      0.221          1.126***      
## 3 Both      0.0000   0.104         -0.503        -0.059         4.036         
## # ℹ 4 more variables: `Cash/TA (ord)` <chr>, `Cash/TA (ext)` <chr>,
## #   `N (ord)` <chr>, `N (ext)` <chr>
save_kbl_latex(table10, "table10_extreme_by_facility",
               caption = "Cross-facility comparison of extreme vs ordinary borrower coefficients.")

5.7 Table 11 — Crisis vs Arbitrage (falsification test)

In the arbitrage window the run equilibrium is dead (BTFP rate < IORB, so banks borrow for arbitrage, not stress). Theory predicts c3 ≈ 0 there, and c3 > 0 in the crisis window.

f_crisis <- as.formula(paste("borrowed ~", rhs_base))
f_arb    <- as.formula(paste("borrowed_arb ~", rhs_base))

m_crisis <- fit_model(f_crisis, df_reg)
m_arb    <- fit_model(f_arb,    df_arb)

models_T11 <- list(Crisis = m_crisis, Arbitrage = m_arb)
table11 <- build_coef_table(models_T11, var_order, var_labels)

table11 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 3
##    Variable              Crisis      Arbitrage  
##    <chr>                 <chr>       <chr>      
##  1 "MTM loss (total)"    "0.023***"  "0.013**"  
##  2 ""                    "(0.006)"   "(0.006)"  
##  3 "Uninsured leverage"  "0.021***"  "0.016**"  
##  4 ""                    "(0.007)"   "(0.006)"  
##  5 "MTM × Uninsured Lev" "0.019***"  "0.009*"   
##  6 ""                    "(0.005)"   "(0.005)"  
##  7 "Ln(assets)"          "0.099***"  "0.057***" 
##  8 ""                    "(0.007)"   "(0.007)"  
##  9 "Cash / TA"           "-0.022***" "-0.043***"
## 10 ""                    "(0.006)"   "(0.005)"  
## 11 "Loan / Deposit"      "-0.027***" "-0.038***"
## 12 ""                    "(0.006)"   "(0.006)"  
## 13 "Book equity / TA"    "-0.012**"  "-0.008"   
## 14 ""                    "(0.005)"   "(0.006)"  
## 15 "Wholesale funding"   "0.040***"  "0.060***" 
## 16 ""                    "(0.007)"   "(0.007)"  
## 17 "ROA"                 "0.003"     "-0.022***"
## 18 ""                    "(0.006)"   "(0.005)"  
## 19 "Constant"            "0.185***"  "0.166***" 
## 20 ""                    "(0.005)"   "(0.005)"  
## 21 ""                    ""          ""         
## 22 "N"                   "4,632"     "4,531"    
## 23 "R²"                  "0.113"     "0.093"    
## 24 "Mean of DV"          "0.1857"    "0.1691"
save_kbl_latex(table11, sprintf("table11_crisis_vs_arbitrage_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Crisis vs arbitrage-window comparison (%s): falsification test for c3 ≈ 0 in arbitrage window.",
                 toupper(MODEL_FAMILY)))

5.8 Table 12 — Crisis vs Arbitrage (DW only, btfp only, both)

# ══════════════════════════════════════════════════════════════════════
# Table 11b — Crisis vs Arbitrage by borrower type (DW Only / BTFP Only / Both)
# Six-column structure matching Table 3's borrower classification
# ══════════════════════════════════════════════════════════════════════

# ── Build three crisis DVs from the 2022Q4 sample ──────────────────
df_reg <- df_reg %>%
  mutate(
    dv_crisis_dw    = as.integer(used_dw == 1 & used_btfp == 0),
    dv_crisis_btfp  = as.integer(used_btfp == 1 & used_dw == 0),
    dv_crisis_both  = as.integer(used_dw == 1 & used_btfp == 1)
  )

cat(sprintf("Crisis window (N = %s):\n", fmt(nrow(df_reg))))
## Crisis window (N = 4,678):
cat(sprintf("  DW Only   : %s\n", fmt(sum(df_reg$dv_crisis_dw))))
##   DW Only   : 341
cat(sprintf("  BTFP Only : %s\n", fmt(sum(df_reg$dv_crisis_btfp))))
##   BTFP Only : 412
cat(sprintf("  Both      : %s\n", fmt(sum(df_reg$dv_crisis_both))))
##   Both      : 107
# ── Build three arbitrage DVs from the 2023Q3 sample ───────────────
# Requires DW arbitrage-window loan data as well; construct it here
dw_arb_bank <- dw_clean %>%
  filter(dw_loan_date >= ARB_START, dw_loan_date <= ARB_END) %>%
  group_by(rssd_id) %>%
  summarise(dw_arb_amount = sum(dw_loan_amount, na.rm = TRUE) / 1000,
            .groups = "drop") %>%
  rename(idrssd = rssd_id)

df_arb <- df_arb %>%
  left_join(dw_arb_bank, by = "idrssd") %>%
  mutate(
    dw_arb_amount   = replace_na(dw_arb_amount, 0),
    used_dw_arb     = as.integer(dw_arb_amount   > 0),
    used_btfp_arb   = as.integer(btfp_arb_amount > 0),
    dv_arb_dw       = as.integer(used_dw_arb   == 1 & used_btfp_arb == 0),
    dv_arb_btfp     = as.integer(used_btfp_arb == 1 & used_dw_arb   == 0),
    dv_arb_both     = as.integer(used_dw_arb   == 1 & used_btfp_arb == 1)
  )

cat(sprintf("\nArbitrage window (N = %s):\n", fmt(nrow(df_arb))))
## 
## Arbitrage window (N = 4,604):
cat(sprintf("  DW Only   : %s\n", fmt(sum(df_arb$dv_arb_dw))))
##   DW Only   : 257
cat(sprintf("  BTFP Only : %s\n", fmt(sum(df_arb$dv_arb_btfp))))
##   BTFP Only : 655
cat(sprintf("  Both      : %s\n", fmt(sum(df_arb$dv_arb_both))))
##   Both      : 111
# ── Fit six regressions ────────────────────────────────────────────
f_cr_dw    <- as.formula(paste("dv_crisis_dw   ~", rhs_base))
f_cr_btfp  <- as.formula(paste("dv_crisis_btfp ~", rhs_base))
f_cr_both  <- as.formula(paste("dv_crisis_both ~", rhs_base))
f_arb_dw   <- as.formula(paste("dv_arb_dw      ~", rhs_base))
f_arb_btfp <- as.formula(paste("dv_arb_btfp    ~", rhs_base))
f_arb_both <- as.formula(paste("dv_arb_both    ~", rhs_base))

models_T11b <- list(
  `Crisis — DW Only`    = fit_model(f_cr_dw,    df_reg),
  `Crisis — BTFP Only`  = fit_model(f_cr_btfp,  df_reg),
  `Crisis — Both`       = fit_model(f_cr_both,  df_reg),
  `Arbitrage — DW Only` = fit_model(f_arb_dw,   df_arb),
  `Arbitrage — BTFP Only` = fit_model(f_arb_btfp, df_arb),
  `Arbitrage — Both`    = fit_model(f_arb_both, df_arb)
)

table11b <- build_coef_table(models_T11b, var_order, var_labels)
table11b %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable              `Crisis — DW Only` `Crisis — BTFP Only` `Crisis — Both`
##    <chr>                 <chr>              <chr>                <chr>          
##  1 "MTM loss (total)"    "0.007"            "0.012**"            "0.004"        
##  2 ""                    "(0.005)"          "(0.005)"            "(0.003)"      
##  3 "Uninsured leverage"  "0.001"            "0.011**"            "0.008**"      
##  4 ""                    "(0.005)"          "(0.005)"            "(0.003)"      
##  5 "MTM × Uninsured Lev" "0.007*"           "0.005"              "0.007**"      
##  6 ""                    "(0.004)"          "(0.004)"            "(0.003)"      
##  7 "Ln(assets)"          "0.051***"         "0.025***"           "0.024***"     
##  8 ""                    "(0.006)"          "(0.005)"            "(0.004)"      
##  9 "Cash / TA"           "0.004"            "-0.024***"          "-0.002"       
## 10 ""                    "(0.004)"          "(0.004)"            "(0.002)"      
## 11 "Loan / Deposit"      "0.001"            "-0.020***"          "-0.007***"    
## 12 ""                    "(0.005)"          "(0.005)"            "(0.003)"      
## 13 "Book equity / TA"    "-0.001"           "-0.012***"          "0.001"        
## 14 ""                    "(0.004)"          "(0.004)"            "(0.002)"      
## 15 "Wholesale funding"   "0.005"            "0.026***"           "0.008***"     
## 16 ""                    "(0.005)"          "(0.005)"            "(0.003)"      
## 17 "ROA"                 "0.005"            "0.002"              "-0.004*"      
## 18 ""                    "(0.004)"          "(0.004)"            "(0.002)"      
## 19 "Constant"            "0.074***"         "0.088***"           "0.024***"     
## 20 ""                    "(0.004)"          "(0.004)"            "(0.002)"      
## 21 ""                    ""                 ""                   ""             
## 22 "N"                   "4,632"            "4,632"              "4,632"        
## 23 "R²"                  "0.041"            "0.042"              "0.039"        
## 24 "Mean of DV"          "0.0736"           "0.0889"             "0.0231"       
## # ℹ 3 more variables: `Arbitrage — DW Only` <chr>,
## #   `Arbitrage — BTFP Only` <chr>, `Arbitrage — Both` <chr>
save_kbl_latex(table11, sprintf("table11b_crisis_vs_arbitrage_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Falsification test (%s): crisis vs arbitrage-window comparison by borrower type. Theory predicts MTM × Uninsured Lev > 0 in crisis cells and ≈ 0 in arbitrage cells.",
                 toupper(MODEL_FAMILY)))

INSURED leverage

# ══════════════════════════════════════════════════════════════════════
# Table 6b — Main extensive-margin regression with INSURED leverage
# Specification check: the DSSW mechanism should operate through UNINSURED
# leverage, not insured. The MTM × Insured Lev coefficient should be
# substantially weaker (or null) compared to the MTM × Uninsured Lev
# coefficient in Table 6.
# ══════════════════════════════════════════════════════════════════════

# Build insured leverage variable if not present
if (!"di_ta_w_z" %in% names(df_reg)) {
  df_reg <- df_reg %>%
    mutate(di_ta = 100 * safe_div(insured_deposit, total_asset, 0)) %>%
    wz_cols("di_ta")
}

# Swap du_ta_w_z → di_ta_w_z in the RHS
rhs_insured <- "ell_w_z * di_ta_w_z + log_ta_w_z + cash_ta_w_z + loan_to_dep_w_z + eq_ta_w_z + wholesale_ta_w_z + roa_w_z"

models_T6b <- list()
for (nm in names(dep_vars)) {
  f <- as.formula(paste(dep_vars[[nm]], "~", rhs_insured))
  models_T6b[[nm]] <- fit_model(f, df_reg)
}

var_order_ins <- c("ell_w_z", "di_ta_w_z", "ell_w_z:di_ta_w_z",
                   "log_ta_w_z", "cash_ta_w_z", "loan_to_dep_w_z",
                   "eq_ta_w_z", "wholesale_ta_w_z", "roa_w_z",
                   "(Intercept)")

var_labels_ins <- c(
  "ell_w_z"            = "MTM loss (total)",
  "di_ta_w_z"          = "Insured leverage",
  "ell_w_z:di_ta_w_z"  = "MTM × Insured Lev",
  "log_ta_w_z"         = "Ln(assets)",
  "cash_ta_w_z"        = "Cash / TA",
  "loan_to_dep_w_z"    = "Loan / Deposit",
  "eq_ta_w_z"          = "Book equity / TA",
  "wholesale_ta_w_z"   = "Wholesale funding",
  "roa_w_z"            = "ROA",
  "(Intercept)"        = "Constant"
)

table6b <- build_coef_table(models_T6b, var_order_ins, var_labels_ins)
table6b %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 "MTM loss (t… "0.000"               "-0.002"            "0.010**"            
##  2 ""            "(0.002)"             "(0.002)"           "(0.005)"            
##  3 "Insured lev… "0.001"               "-0.001"            "-0.012**"           
##  4 ""            "(0.003)"             "(0.002)"           "(0.006)"            
##  5 "MTM × Insur… "-0.001"              "0.001"             "-0.010**"           
##  6 ""            "(0.002)"             "(0.002)"           "(0.004)"            
##  7 "Ln(assets)"  "0.014***"            "0.015***"          "0.066***"           
##  8 ""            "(0.003)"             "(0.003)"           "(0.006)"            
##  9 "Cash / TA"   "-0.004*"             "-0.004**"          "0.002"              
## 10 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 11 "Loan / Depo… "-0.005*"             "-0.005**"          "-0.008"             
## 12 ""            "(0.003)"             "(0.002)"           "(0.005)"            
## 13 "Book equity… "0.002"               "-0.001"            "-0.005"             
## 14 ""            "(0.002)"             "(0.001)"           "(0.004)"            
## 15 "Wholesale f… "0.006**"             "0.008***"          "0.010*"             
## 16 ""            "(0.003)"             "(0.003)"           "(0.005)"            
## 17 "ROA"         "-0.002"              "-0.002"            "0.002"              
## 18 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 19 "Constant"    "0.015***"            "0.014***"          "0.087***"           
## 20 ""            "(0.002)"             "(0.002)"           "(0.004)"            
## 21 ""            ""                    ""                  ""                   
## 22 "N"           "4,632"               "4,632"             "4,632"              
## 23 "R²"          "0.015"               "0.023"             "0.068"              
## 24 "Mean of DV"  "0.0155"              "0.0145"            "0.0857"             
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(table6b, sprintf("table6b_insured_leverage_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Specification check (%s): main regression with INSURED leverage (di_ta) replacing uninsured leverage. Theory predicts MTM × Insured Lev weaker than MTM × Uninsured Lev in Table 6.",
                 toupper(MODEL_FAMILY)))

# ── Side-by-side comparison of the two interactions ────────────────
cat("\nInteraction coefficient comparison (Table 6 vs Table 6b):\n")
## 
## Interaction coefficient comparison (Table 6 vs Table 6b):
tibble(
  Cell = names(models_T6),
  `MTM × Uninsured (T6)` = map_chr(models_T6, function(m) {
    tm <- get_robust_tidy(m); i <- which(tm$term == "ell_w_z:du_ta_w_z")
    if (length(i) == 0) "" else fmt_est(tm$estimate[i], tm$p.value[i])
  }),
  `MTM × Insured (T6b)` = map_chr(models_T6b, function(m) {
    tm <- get_robust_tidy(m); i <- which(tm$term == "ell_w_z:di_ta_w_z")
    if (length(i) == 0) "" else fmt_est(tm$estimate[i], tm$p.value[i])
  })
) %>% as_tibble() %>% print(n = Inf)
## # A tibble: 6 × 3
##   Cell                  `MTM × Uninsured (T6)` `MTM × Insured (T6b)`
##   <chr>                 <chr>                  <chr>                
## 1 P0: Pre-Crisis | DW   0.002                  -0.001               
## 2 P1: SVB Week | DW     0.001                  0.001                
## 3 P2: SVB to FRC | DW   0.013***               -0.010**             
## 4 P2: SVB to FRC | BTFP 0.012***               -0.006               
## 5 P3: FRC Week | DW     0.005**                -0.004*              
## 6 P3: FRC Week | BTFP   0.005*                 -0.003
# ══════════════════════════════════════════════════════════════════════
# Table A3 — Returning-borrower LPM (regression counterpart of Table A1)
# Among borrowers in cell (p, f), what predicts being a RETURNING borrower?
# Sample: only banks that borrowed in cell (p, f). P0 dropped because
# all P0 borrowers are new by construction.
# ══════════════════════════════════════════════════════════════════════

# Build DV for each cell: 1 if returning, 0 if new; NA if not in cell
df_returning <- df_reg  # Start with df_reg, then add per-cell DVs
for (i in seq_len(nrow(col_specs))) {
  cs <- col_specs[i, ]
  dep_name <- paste0("ret_",
                     gsub("[^a-z0-9]", "_", tolower(cs$col_id)))
  ids_cell <- banks_in_cell(cs$period, cs$facility)
  prior    <- prior_set(cs$period)
  df_returning[[dep_name]] <- dplyr::case_when(
    !(df_returning$idrssd %in% ids_cell) ~ NA_integer_,
    df_returning$idrssd %in% prior       ~ 1L,
    TRUE                                  ~ 0L
  )
}

# Fit only cells with variation in the DV (drops P0-DW, where all are new)
models_T_ret <- list()
for (i in seq_len(nrow(col_specs))) {
  cs <- col_specs[i, ]
  cid <- cs$col_id
  dep_name <- paste0("ret_",
                     gsub("[^a-z0-9]", "_", tolower(cid)))
  sub <- df_returning %>% filter(!is.na(.data[[dep_name]]))
  if (var(sub[[dep_name]], na.rm = TRUE) == 0) {
    cat(sprintf("Skipping %s: no variation (all %s).\n",
                cid,
                if (mean(sub[[dep_name]]) == 0) "new" else "returning"))
    next
  }
  f <- as.formula(paste(dep_name, "~", rhs_base))
  models_T_ret[[cid]] <- fit_model(f, sub)
}
## Skipping P0: Pre-Crisis | DW: no variation (all new).
tableA3 <- build_coef_table(models_T_ret, var_order, var_labels)
tableA3 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 6
##    Variable     `P1: SVB Week | DW` `P2: SVB to FRC | DW` P2: SVB to FRC | BTF…¹
##    <chr>        <chr>               <chr>                 <chr>                 
##  1 "MTM loss (… "-0.091"            "-0.052**"            "-0.024"              
##  2 ""           "(0.067)"           "(0.025)"             "(0.016)"             
##  3 "Uninsured … "0.081"             "0.013"               "0.010"               
##  4 ""           "(0.090)"           "(0.019)"             "(0.015)"             
##  5 "MTM × Unin… "0.053"             "-0.010"              "-0.003"              
##  6 ""           "(0.082)"           "(0.016)"             "(0.011)"             
##  7 "Ln(assets)" "-0.107"            "0.024"               "0.042***"            
##  8 ""           "(0.081)"           "(0.021)"             "(0.015)"             
##  9 "Cash / TA"  "-0.084"            "-0.061**"            "-0.024"              
## 10 ""           "(0.119)"           "(0.025)"             "(0.028)"             
## 11 "Loan / Dep… "-0.210***"         "-0.046"              "-0.027"              
## 12 ""           "(0.079)"           "(0.031)"             "(0.019)"             
## 13 "Book equit… "0.174"             "-0.031"              "0.011"               
## 14 ""           "(0.165)"           "(0.032)"             "(0.023)"             
## 15 "Wholesale … "0.066"             "0.059***"            "0.020"               
## 16 ""           "(0.064)"           "(0.021)"             "(0.014)"             
## 17 "ROA"        "-0.215***"         "-0.022"              "-0.010"              
## 18 ""           "(0.074)"           "(0.022)"             "(0.015)"             
## 19 "Constant"   "0.649***"          "0.115***"            "0.028*"              
## 20 ""           "(0.107)"           "(0.023)"             "(0.016)"             
## 21 ""           ""                  ""                    ""                    
## 22 "N"          "67"                "397"                 "460"                 
## 23 "R²"         "0.217"             "0.060"               "0.049"               
## 24 "Mean of DV" "0.5224"            "0.1637"              "0.0630"              
## # ℹ abbreviated name: ¹​`P2: SVB to FRC | BTFP`
## # ℹ 2 more variables: `P3: FRC Week | DW` <chr>, `P3: FRC Week | BTFP` <chr>
save_kbl_latex(tableA3, sprintf("tableA3_returning_lpm_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Returning-borrower regression (%s): conditional on borrowing in cell, LPM for returning status.",
                 toupper(MODEL_FAMILY)))
# ══════════════════════════════════════════════════════════════════════
# Table A4 — Repeat-borrower LPM (regression counterpart of Table A2)
# Among borrowers in cell (p, f), what predicts being a REPEAT borrower?
# Sample: only banks that borrowed in cell (p, f).
# ══════════════════════════════════════════════════════════════════════

# Attach cell_types (built in Table A2) for the repeat/one-time flag
# NOTE: cell_types must exist in scope; if running this chunk standalone,
# re-run Table A2 construction first.

df_repeat <- df_reg
for (i in seq_len(nrow(col_specs))) {
  cs <- col_specs[i, ]
  dep_name <- paste0("rep_",
                     gsub("[^a-z0-9]", "_", tolower(cs$col_id)))
  cell_flags <- cell_types %>%
    filter(period == cs$period, facility == cs$facility) %>%
    transmute(idrssd = rssd_id,
              repeat_flag = as.integer(type == "Repeat"))
  # Left-join so non-borrowers get NA
  df_repeat[[dep_name]] <- cell_flags$repeat_flag[match(df_repeat$idrssd,
                                                        cell_flags$idrssd)]
}

# Fit only cells with variation
models_T_rep <- list()
for (i in seq_len(nrow(col_specs))) {
  cs <- col_specs[i, ]
  cid <- cs$col_id
  dep_name <- paste0("rep_",
                     gsub("[^a-z0-9]", "_", tolower(cid)))
  sub <- df_repeat %>% filter(!is.na(.data[[dep_name]]))
  if (var(sub[[dep_name]], na.rm = TRUE) == 0) {
    cat(sprintf("Skipping %s: no variation.\n", cid))
    next
  }
  f <- as.formula(paste(dep_name, "~", rhs_base))
  models_T_rep[[cid]] <- fit_model(f, sub)
}

tableA4 <- build_coef_table(models_T_rep, var_order, var_labels)
tableA4 %>% as_tibble() %>% print(n = Inf)
## # A tibble: 24 × 7
##    Variable      `P0: Pre-Crisis | DW` `P1: SVB Week | DW` `P2: SVB to FRC | DW`
##    <chr>         <chr>                 <chr>               <chr>                
##  1 "MTM loss (t… "-0.033"              "-0.021"            "-0.036"             
##  2 ""            "(0.062)"             "(0.072)"           "(0.029)"            
##  3 "Uninsured l… "0.152***"            "0.097"             "0.065***"           
##  4 ""            "(0.056)"             "(0.090)"           "(0.025)"            
##  5 "MTM × Unins… "0.010"               "-0.014"            "0.026"              
##  6 ""            "(0.062)"             "(0.076)"           "(0.019)"            
##  7 "Ln(assets)"  "0.055"               "-0.122"            "-0.023"             
##  8 ""            "(0.067)"             "(0.085)"           "(0.026)"            
##  9 "Cash / TA"   "-0.104"              "-0.330**"          "-0.103***"          
## 10 ""            "(0.100)"             "(0.131)"           "(0.030)"            
## 11 "Loan / Depo… "-0.157**"            "-0.113"            "-0.077**"           
## 12 ""            "(0.063)"             "(0.098)"           "(0.036)"            
## 13 "Book equity… "-0.128**"            "0.111"             "-0.031"             
## 14 ""            "(0.062)"             "(0.186)"           "(0.041)"            
## 15 "Wholesale f… "0.053"               "0.080"             "0.102***"           
## 16 ""            "(0.053)"             "(0.063)"           "(0.024)"            
## 17 "ROA"         "-0.075"              "-0.103"            "-0.001"             
## 18 ""            "(0.076)"             "(0.091)"           "(0.029)"            
## 19 "Constant"    "0.290***"            "0.372**"           "0.231***"           
## 20 ""            "(0.087)"             "(0.141)"           "(0.029)"            
## 21 ""            ""                    ""                  ""                   
## 22 "N"           "72"                  "67"                "397"                
## 23 "R²"          "0.223"               "0.142"             "0.106"              
## 24 "Mean of DV"  "0.4444"              "0.4478"            "0.2846"             
## # ℹ 3 more variables: `P2: SVB to FRC | BTFP` <chr>, `P3: FRC Week | DW` <chr>,
## #   `P3: FRC Week | BTFP` <chr>
save_kbl_latex(tableA4, sprintf("tableA4_repeat_lpm_%s", MODEL_FAMILY),
               caption = sprintf(
                 "Repeat-borrower regression (%s): conditional on borrowing in cell, LPM for repeat status.",
                 toupper(MODEL_FAMILY)))

5.9 Summary: how to switch model family

# To regenerate all LPM tables as logit:
# 1. At the top of the Rmd, set:
#    MODEL_FAMILY <- "logit"
# 2. Re-knit. All extensive-margin tables (6, 7, 8, 11) will regenerate.
# Tables 9, 10, 12 remain lm() because their DV is continuous.
#
# Saved files use family suffix: e.g., table6_main_lpm.tex, table6_main_logit.tex
# so you can compare specifications side-by-side.