# ============================================================================
# LOAD PACKAGES
# ============================================================================
library(tidyverse)
library(data.table)
library(lubridate)
library(fixest)
library(sandwich)
library(lmtest)
library(broom)
library(margins)
library(pROC)
library(sampleSelection)
library(modelsummary)
library(kableExtra)
library(patchwork)
library(scales)
library(viridis)
library(psych)

cat("All packages loaded successfully.\n")

All packages loaded successfully.

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

# CRITICAL: Winsorization function - applied to ALL continuous variables
winsorize <- function(x, probs = c(0.01, 0.99)) {
  if (all(is.na(x))) return(x)
  q <- quantile(x, probs = probs, na.rm = TRUE, names = FALSE)
  pmax(pmin(x, q[2]), q[1])
}

# Safe division (handles division by zero)
safe_div <- function(num, denom, default = NA_real_) {
  ifelse(is.na(denom) | denom == 0, default, num / denom)
}

# Significance stars
add_stars <- function(pval) {
  case_when(pval < 0.01 ~ "***", pval < 0.05 ~ "**", pval < 0.10 ~ "*", TRUE ~ "")
}

# Publication theme
theme_pub <- function(base_size = 12) {
  theme_minimal(base_size = base_size) +
    theme(
      plot.title = element_text(face = "bold", size = base_size + 2),
      plot.subtitle = element_text(color = "gray40"),
      legend.position = "bottom",
      panel.grid.minor = element_blank(),
      axis.title = element_text(face = "bold")
    )
}

# Colors
COLORS <- list(
  btfp = "#2E86AB", dw = "#A23B72", both = "#F18F01", neither = "gray70",
  acute = "#FC9272", post = "#A1D99B", arb = "#9ECAE1"
)

# Format coefficient with SE
format_coef <- function(est, se, pval) {
  sprintf("%.4f%s\n(%.4f)", est, add_stars(pval), se)
}

1 Executive Summary

1.1 Research Questions Addressed

This analysis provides causal identification for the following questions:

# Research Question Identification Strategy Key Finding
1 Did BTFP’s par valuation create systematic selection? Compare MTM(BTFP-eligible) vs MTM(Non-eligible) effects ✓ Table 4-5
2 What does pre-BTFP DW usage reveal? Placebo test: MTM shouldn’t predict pre-BTFP DW ✓ Table 6
3 Did vulnerable (insolvent) banks access Fed facilities? Jiang et al. insolvency measures ✓ Table 7-8
4 Did borrowing determinants change over time? Period-specific entry models + equality tests âś“ Table 9-10
5 Did banks “max out” BTFP before turning to DW? Collateral utilization analysis ✓ Table 14-15
6 Who borrowed more conditional on participation? Two-part model (intensive margin) âś“ Table 11-12
7 Was DW insufficient? What made BTFP necessary? Capacity gap analysis âś“ Table 16

1.2 Identification Strategy

Core Identification: BTFP lends at par value while DW applies market-value haircuts

Testable Predictions:

  1. MTM(BTFP-eligible) should predict BTFP selection (captures subsidy)
  2. MTM(Non-eligible) should NOT predict BTFP (no par valuation benefit)
  3. Pre-BTFP DW usage should show weaker MTM effects (placebo period)
  4. Insolvent banks (negative adjusted equity) should access BTFP more

1.3 Period Definitions

periods <- tribble(
  ~period_num, ~period_name, ~start_date, ~end_date, ~description,
  1, "Pre-BTFP",   "2023-03-01", "2023-03-10", "SVB crisis onset, DW only",
  2, "Acute",      "2023-03-13", "2023-05-01", "Acute crisis phase",
  3, "Post-Acute", "2023-05-02", "2023-10-31", "Stabilization phase",
  4, "Arbitrage",  "2023-11-01", "2024-01-24", "BTFP rate < IORB",
  5, "Wind-down",  "2024-01-25", "2024-03-11", "BTFP closing"
) %>% mutate(across(c(start_date, end_date), as.Date))

periods %>%
  kable(col.names = c("Period", "Name", "Start", "End", "Description"),
        caption = "Analysis Period Definitions") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Analysis Period Definitions
Period Name Start End Description
1 Pre-BTFP 2023-03-01 2023-03-10 SVB crisis onset, DW only
2 Acute 2023-03-13 2023-05-01 Acute crisis phase
3 Post-Acute 2023-05-02 2023-10-31 Stabilization phase
4 Arbitrage 2023-11-01 2024-01-24 BTFP rate < IORB
5 Wind-down 2024-01-25 2024-03-11 BTFP closing

2 Data Preparation

# ============================================================================
# LOAD DATA
# ============================================================================
BASE_PATH <- "C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025"
DATA_PROC <- file.path(BASE_PATH, "01_data/processed")
BASELINE_DATE <- "2022Q4"

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

btfp_loans <- read_csv(file.path(DATA_PROC, "btfp_loan_bank_only.csv"), show_col_types = FALSE) %>%
  mutate(rssd_id = as.character(rssd_id), btfp_loan_date = mdy(btfp_loan_date))

dw_loans <- read_csv(file.path(DATA_PROC, "dw_loan_bank_2023.csv"), show_col_types = FALSE) %>%
  mutate(rssd_id = as.character(rssd_id), dw_loan_date = ymd(dw_loan_date))

cat("=== DATA LOADED ===\n")

=== DATA LOADED ===

cat("Call Report:", nrow(call_q), "| BTFP Loans:", nrow(btfp_loans), "| DW Loans:", nrow(dw_loans), "\n")

Call Report: 61002 | BTFP Loans: 6734 | DW Loans: 20219

# Assign periods to loans
assign_period <- function(date, periods_df) {
  sapply(date, function(d) {
    if (is.na(d)) return(NA_integer_)
    match <- periods_df %>% filter(d >= start_date & d <= end_date) %>% pull(period_num)
    if (length(match) == 0) NA_integer_ else match[1]
  })
}

btfp_loans <- btfp_loans %>% mutate(period = assign_period(btfp_loan_date, periods))
dw_loans <- dw_loans %>% mutate(period = assign_period(dw_loan_date, periods))
# Exclude failed banks and G-SIBs
excluded <- call_q %>%
  filter(quarter == BASELINE_DATE, failed_bank == 1 | gsib == 1) %>%
  pull(idrssd)

btfp_filt <- btfp_loans %>% filter(!rssd_id %in% excluded)
dw_filt <- dw_loans %>% filter(!rssd_id %in% excluded)

cat("Excluded:", length(excluded), "banks (failed + G-SIBs)\n")

Excluded: 41 banks (failed + G-SIBs)

# ============================================================================
# AGGREGATE BORROWER-LEVEL DATA
# ============================================================================

# BTFP borrowers (post-announcement)
btfp_agg <- btfp_filt %>%
  filter(!is.na(period), period >= 2) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp = 1L,
    btfp_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_first_date = min(btfp_loan_date),
    btfp_first_period = first(period[btfp_loan_date == min(btfp_loan_date)]),
    btfp_n_loans = n(),
    btfp_collateral = sum(btfp_total_collateral, na.rm = TRUE),
    .groups = "drop"
  ) %>% rename(idrssd = rssd_id)

# DW post-BTFP
dw_post_agg <- dw_filt %>%
  filter(!is.na(period), period >= 2) %>%
  group_by(rssd_id) %>%
  summarise(
    dw = 1L,
    dw_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_first_date = min(dw_loan_date),
    dw_first_period = first(period[dw_loan_date == min(dw_loan_date)]),
    dw_omo_coll = sum(dw_omo_eligible, na.rm = TRUE),
    dw_non_omo_coll = sum(dw_non_omo_eligible, na.rm = TRUE),
    .groups = "drop"
  ) %>% rename(idrssd = rssd_id)

# DW pre-BTFP (PLACEBO - revealed preference / operational readiness)
dw_pre_agg <- dw_filt %>%
  filter(dw_loan_date < as.Date("2023-03-13")) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_pre = 1L,
    dw_pre_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_pre_first_date = min(dw_loan_date),
    .groups = "drop"
  ) %>% rename(idrssd = rssd_id)

cat("BTFP borrowers:", nrow(btfp_agg), "| DW post:", nrow(dw_post_agg), "| DW pre (placebo):", nrow(dw_pre_agg), "\n")

BTFP borrowers: 1316 | DW post: 1077 | DW pre (placebo): 1352

# ============================================================================
# CONSTRUCT ANALYSIS DATASET
# ============================================================================

baseline <- call_q %>%
  filter(quarter == BASELINE_DATE, !idrssd %in% excluded)

# Merge and construct RAW variables first
df_raw <- baseline %>%
  left_join(btfp_agg, by = "idrssd") %>%
  left_join(dw_post_agg, by = "idrssd") %>%
  left_join(dw_pre_agg, by = "idrssd") %>%
  mutate(
    # Fill NAs for binary indicators
    btfp = replace_na(btfp, 0L),
    dw = replace_na(dw, 0L),
    dw_pre = replace_na(dw_pre, 0L),
    
    # ================================================================
    # DEPENDENT VARIABLES
    # ================================================================
    any_fed = as.integer(btfp == 1 | dw == 1),
    both = as.integer(btfp == 1 & dw == 1),
    btfp_only = as.integer(btfp == 1 & dw == 0),
    dw_only = as.integer(btfp == 0 & dw == 1),
    
    facility_choice = factor(
      case_when(both == 1 ~ "Both", btfp_only == 1 ~ "BTFP_Only",
                dw_only == 1 ~ "DW_Only", TRUE ~ "Neither"),
      levels = c("Neither", "BTFP_Only", "DW_Only", "Both")
    ),
    
    # Period-specific entry
    btfp_acute = as.integer(!is.na(btfp_first_period) & btfp_first_period == 2),
    btfp_post = as.integer(!is.na(btfp_first_period) & btfp_first_period == 3),
    btfp_arb = as.integer(!is.na(btfp_first_period) & btfp_first_period == 4),
    
    # DW period-specific
    dw_acute = as.integer(!is.na(dw_first_period) & dw_first_period == 2),
    dw_post = as.integer(!is.na(dw_first_period) & dw_first_period == 3),
    dw_arb = as.integer(!is.na(dw_first_period) & dw_first_period == 4),
    
    # ================================================================
    # RAW KEY VARIABLES (will be winsorized)
    # ================================================================
    # MTM Loss Variables - CRITICAL: Split by collateral eligibility
    mtm_btfp_raw = mtm_loss_omo_eligible_to_total_asset,
    mtm_other_raw = mtm_loss_non_omo_eligible_to_total_asset,
    mtm_total_raw = mtm_loss_to_total_asset,
    
    # Borrowing subsidy (par valuation benefit)
    borrowing_subsidy_raw = mtm_loss_omo_eligible_to_omo_eligible,
    
    # Deposit structure
    uninsured_lev_raw = uninsured_deposit_to_total_asset,
    uninsured_share_raw = uninsured_to_deposit,
    
    # Collateral ratios
    omo_ratio_raw = omo_eligible_to_total_asset,
    non_omo_ratio_raw = non_omo_eligible_to_total_asset,
    
    # ================================================================
    # JIANG ET AL. INSOLVENCY MEASURES (RAW)
    # ================================================================
    mv_asset = mm_asset,
    
    # Adjusted Equity = Book Equity - MTM Losses
    adjusted_equity_raw = book_equity_to_total_asset - mtm_loss_to_total_asset,
    
    # Market value adjustment factor
    mv_adjustment = safe_div(total_asset - mv_asset, mv_asset, 0),
    
    # Insured Deposit Coverage Ratio (IDCR)
    # IDCR = (MV_Assets - s*Uninsured - Insured) / Insured
    idcr_50_raw = safe_div(mv_asset - 0.5 * uninsured_deposit - insured_deposit, insured_deposit),
    idcr_100_raw = safe_div(mv_asset - 1.0 * uninsured_deposit - insured_deposit, insured_deposit),
    
    # Capital ratio under run scenario
    insolvency_50_raw = safe_div(
      (total_asset - total_liability) - 0.5 * uninsured_deposit * mv_adjustment,
      total_asset
    ),
    insolvency_100_raw = safe_div(
      (total_asset - total_liability) - 1.0 * uninsured_deposit * mv_adjustment,
      total_asset
    ),
    
    # ================================================================
    # CONTROL VARIABLES (RAW)
    # ================================================================
    ln_assets_raw = log(total_asset),
    cash_ratio_raw = cash_to_total_asset,
    securities_ratio_raw = security_to_total_asset,
    loan_ratio_raw = total_loan_to_total_asset,
    book_equity_ratio_raw = book_equity_to_total_asset,
    tier1_ratio_raw = tier1cap_to_total_asset,
    roa_raw = roa,
    loan_to_deposit_raw = loan_to_deposit,
    fhlb_ratio_raw = fhlb_to_total_asset,
    
    # Wholesale funding
    wholesale_raw = safe_div(
      fed_fund_purchase + repo + replace_na(other_borrowed_less_than_1yr, 0),
      total_liability, 0
    ) * 100,
    
    # Runable funding
    runable_raw = safe_div(
      uninsured_deposit + fed_fund_purchase + repo + replace_na(other_borrowed_less_than_1yr, 0),
      total_liability, 0
    ) * 100,
    
    # ================================================================
    # INTENSIVE MARGIN (RAW)
    # ================================================================
    btfp_pct_raw = ifelse(btfp == 1, 100 * btfp_amount / (total_asset * 1000), NA_real_),
    dw_pct_raw = ifelse(dw == 1, 100 * dw_amount / (total_asset * 1000), NA_real_),
    
    btfp_util_raw = ifelse(btfp == 1 & omo_eligible > 0,
                            btfp_amount / (omo_eligible * 1000), NA_real_),
    
    # Size category
    size_cat = factor(size_bin, levels = c("small", "large")),
    
    # Prior DW user (revealed preference / operational readiness)
    prior_dw = dw_pre
  )

# ============================================================================
# WINSORIZE ALL CONTINUOUS VARIABLES AT 1-99%
# CRITICAL: Use winsorized versions in ALL analysis
# ============================================================================

df <- df_raw %>%
  mutate(
    # Key explanatory (WINSORIZED)
    mtm_btfp = winsorize(mtm_btfp_raw),
    mtm_other = winsorize(mtm_other_raw),
    mtm_total = winsorize(mtm_total_raw),
    borrowing_subsidy = winsorize(borrowing_subsidy_raw),
    uninsured_lev = winsorize(uninsured_lev_raw),
    uninsured_share = winsorize(uninsured_share_raw),
    
    # Collateral (WINSORIZED)
    omo_ratio = winsorize(omo_ratio_raw),
    non_omo_ratio = winsorize(non_omo_ratio_raw),
    
    # Insolvency measures (WINSORIZED)
    adjusted_equity = winsorize(adjusted_equity_raw),
    idcr_50 = winsorize(idcr_50_raw),
    idcr_100 = winsorize(idcr_100_raw),
    insolvency_50 = winsorize(insolvency_50_raw),
    insolvency_100 = winsorize(insolvency_100_raw),
    
    # Controls (WINSORIZED)
    ln_assets = winsorize(ln_assets_raw),
    cash_ratio = winsorize(cash_ratio_raw),
    securities_ratio = winsorize(securities_ratio_raw),
    loan_ratio = winsorize(loan_ratio_raw),
    book_equity_ratio = winsorize(book_equity_ratio_raw),
    tier1_ratio = winsorize(tier1_ratio_raw),
    roa = winsorize(roa_raw),
    loan_to_deposit = winsorize(loan_to_deposit_raw),
    fhlb_ratio = winsorize(fhlb_ratio_raw),
    wholesale = winsorize(wholesale_raw),
    runable = winsorize(runable_raw),
    
    # Intensive margin (WINSORIZED)
    btfp_pct = winsorize(btfp_pct_raw),
    dw_pct = winsorize(dw_pct_raw),
    btfp_util = winsorize(btfp_util_raw),
    
    # ================================================================
    # DERIVED VARIABLES (from winsorized inputs)
    # ================================================================
    # Run risk interaction
    run_risk = uninsured_lev * mtm_total,
    
    # Insolvency dummies
    mtm_insolvent = as.integer(adjusted_equity < 0),
    insolvent_idcr_50 = as.integer(idcr_50 < 0),
    insolvent_idcr_100 = as.integer(idcr_100 < 0),
    
    # Risk dummies (based on winsorized medians)
    high_uninsured = as.integer(uninsured_lev > median(uninsured_lev, na.rm = TRUE)),
    high_mtm = as.integer(mtm_total > median(mtm_total, na.rm = TRUE)),
    high_mtm_btfp = as.integer(mtm_btfp > median(mtm_btfp, na.rm = TRUE)),
    run_risk_dummy = as.integer(high_uninsured == 1 & high_mtm == 1),
    
    # Maxed out BTFP
    maxed_out = as.integer(!is.na(btfp_util) & btfp_util > 0.90),
    
    # DW capacity gap (what BTFP provided that DW couldn't)
    # = BTFP amount - (what DW would have lent on same collateral with haircuts)
    dw_capacity_gap = ifelse(btfp == 1 & !is.na(btfp_amount) & !is.na(borrowing_subsidy),
                              btfp_amount * borrowing_subsidy / 100, NA_real_)
  )

# Remove raw variables to prevent accidental use
df <- df %>% select(-ends_with("_raw"))

cat("\n=== FINAL SAMPLE (WINSORIZED) ===\n")

=== FINAL SAMPLE (WINSORIZED) ===

cat("Total banks:", nrow(df), "\n")

Total banks: 4696

cat("BTFP users:", sum(df$btfp), "\n")

BTFP users: 1305

cat("DW users:", sum(df$dw), "\n")

DW users: 1067

cat("Both:", sum(df$both), "\n")

Both: 425

cat("DW pre-BTFP (placebo):", sum(df$dw_pre), "\n")

DW pre-BTFP (placebo): 1308

cat("MTM insolvent:", sum(df$mtm_insolvent, na.rm = TRUE), "\n")

MTM insolvent: 864

3 Summary Statistics

3.1 Table 1: Full Sample Statistics (Winsorized)

# ============================================================================
# TABLE 1: SUMMARY STATISTICS - ALL WINSORIZED
# ============================================================================

sum_vars <- c("ln_assets", "mtm_total", "mtm_btfp", "mtm_other", "borrowing_subsidy",
              "uninsured_lev", "uninsured_share", "run_risk",
              "omo_ratio", "non_omo_ratio",
              "adjusted_equity", "idcr_50", "idcr_100", "insolvency_50", "insolvency_100",
              "cash_ratio", "securities_ratio", "loan_ratio",
              "book_equity_ratio", "loan_to_deposit", "wholesale", "fhlb_ratio", "roa")

sum_labels <- c("Log(Assets)", "MTM Loss (Total)", "MTM Loss (BTFP-Eligible)",
                "MTM Loss (Non-BTFP)", "Borrowing Subsidy",
                "Uninsured/Assets", "Uninsured/Deposits", "Run Risk (MTMĂ—Uninsured)",
                "OMO-Eligible/Assets", "Non-OMO/Assets",
                "Adjusted Equity", "IDCR (50% run)", "IDCR (100% run)",
                "Insolvency (50%)", "Insolvency (100%)",
                "Cash/Assets", "Securities/Assets", "Loans/Assets",
                "Book Equity/Assets", "Loans/Deposits", "Wholesale Funding", "FHLB/Assets", "ROA")

table1 <- df %>%
  select(all_of(sum_vars)) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "value") %>%
  group_by(variable) %>%
  summarise(
    N = sum(!is.na(value)),
    Mean = mean(value, na.rm = TRUE),
    SD = sd(value, na.rm = TRUE),
    Min = min(value, na.rm = TRUE),
    P25 = quantile(value, 0.25, na.rm = TRUE),
    Median = median(value, na.rm = TRUE),
    P75 = quantile(value, 0.75, na.rm = TRUE),
    Max = max(value, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(variable = factor(variable, levels = sum_vars)) %>%
  arrange(variable) %>%
  mutate(Label = sum_labels)

table1 %>%
  select(Label, N, Mean, SD, Min, P25, Median, P75, Max) %>%
  mutate(across(where(is.numeric), ~round(., 3))) %>%
  kable(caption = "**Table 1: Summary Statistics - Winsorized at 1-99%**",
        col.names = c("Variable", "N", "Mean", "SD", "Min", "P25", "Median", "P75", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 11) %>%
  pack_rows("Size", 1, 1) %>%
  pack_rows("MTM Loss Variables", 2, 5) %>%
  pack_rows("Deposit Risk", 6, 8) %>%
  pack_rows("Collateral", 9, 10) %>%
  pack_rows("Insolvency Measures (Jiang et al.)", 11, 15) %>%
  pack_rows("Controls", 16, 23) %>%
  footnote(general = "All continuous variables winsorized at 1st and 99th percentiles.")
Table 1: Summary Statistics - Winsorized at 1-99%
Variable N Mean SD Min P25 Median P75 Max
Size
Log(Assets) 4696 12.808 1.433 9.773 11.850 12.669 13.583 17.362
MTM Loss Variables
MTM Loss (Total) 4678 5.403 2.248 0.101 3.804 5.272 6.961 10.729
MTM Loss (BTFP-Eligible) 4678 0.611 0.744 0.000 0.070 0.341 0.866 3.609
MTM Loss (Non-BTFP) 4678 4.578 2.098 0.000 3.079 4.382 5.974 9.999
Borrowing Subsidy 4282 6.985 5.057 -0.034 3.205 6.537 9.849 25.768
Deposit Risk
Uninsured/Assets 4696 23.175 12.035 0.000 14.805 21.932 30.165 60.372
Uninsured/Deposits 4696 27.309 14.227 0.000 17.478 25.607 34.981 72.311
Run Risk (MTMĂ—Uninsured) 4678 122.203 75.349 0.000 67.492 110.702 163.291 608.515
Collateral
OMO-Eligible/Assets 4696 9.730 9.931 0.000 2.270 6.733 14.079 46.779
Non-OMO/Assets 4696 75.059 15.712 6.580 69.048 79.273 85.953 94.356
Insolvency Measures (Jiang et al.)
Adjusted Equity 4678 5.191 10.044 -6.202 0.972 3.808 6.729 79.391
IDCR (50% run) 4632 0.340 0.445 -0.255 0.148 0.248 0.392 3.530
IDCR (100% run) 4632 0.104 0.290 -0.603 -0.012 0.072 0.165 1.980
Insolvency (50%) 4696 0.094 0.101 -0.029 0.055 0.077 0.102 0.815
Insolvency (100%) 4696 0.080 0.109 -0.144 0.040 0.067 0.097 0.815
Controls
Cash/Assets 4696 8.618 9.735 0.816 2.696 5.185 10.488 57.254
Securities/Assets 4696 24.422 15.973 0.000 12.163 22.266 34.347 71.312
Loans/Assets 4696 60.297 18.481 0.000 49.821 62.840 74.564 88.931
Book Equity/Assets 4696 10.770 9.613 2.024 7.236 9.003 11.182 80.762
Loans/Deposits 4696 71.184 23.888 0.000 56.972 72.694 87.894 125.874
Wholesale Funding 4696 0.857 2.120 0.000 0.000 0.000 0.437 12.286
FHLB/Assets 4696 2.675 4.124 0.000 0.000 0.306 4.121 20.319
ROA 4696 1.081 0.924 -1.510 0.694 1.015 1.337 7.152
Note:
All continuous variables winsorized at 1st and 99th percentiles.

3.2 Table 2: Comparison by Facility Choice

# ============================================================================
# TABLE 2: UNIVARIATE COMPARISON BY FACILITY CHOICE
# ============================================================================

comp_vars <- c("ln_assets", "mtm_total", "mtm_btfp", "mtm_other", "borrowing_subsidy",
               "uninsured_lev", "run_risk", "adjusted_equity", "idcr_100",
               "omo_ratio", "non_omo_ratio", "cash_ratio", "book_equity_ratio", "fhlb_ratio")

comp_labels <- c("Log(Assets)", "MTM Loss (Total)", "MTM Loss (BTFP)",
                 "MTM Loss (Non-BTFP)", "Borrowing Subsidy",
                 "Uninsured Leverage", "Run Risk", "Adjusted Equity", "IDCR (100%)",
                 "OMO Ratio", "Non-OMO Ratio", "Cash Ratio", "Book Equity", "FHLB Ratio")

# Group counts
grp_n <- df %>% count(facility_choice) %>% deframe()

# Group means
grp_means <- df %>%
  group_by(facility_choice) %>%
  summarise(across(all_of(comp_vars), ~mean(., na.rm = TRUE)), .groups = "drop")

# T-tests vs Neither
neither_df <- df %>% filter(facility_choice == "Neither")

ttest_list <- map_dfr(comp_vars, function(v) {
  result <- tibble(variable = v)
  for (grp in c("BTFP_Only", "DW_Only", "Both")) {
    grp_vals <- df %>% filter(facility_choice == grp) %>% pull(!!sym(v))
    neither_vals <- neither_df %>% pull(!!sym(v))
    if (sum(!is.na(grp_vals)) > 2 & sum(!is.na(neither_vals)) > 2) {
      tt <- t.test(grp_vals, neither_vals)
      result[[paste0(grp, "_mean")]] <- mean(grp_vals, na.rm = TRUE)
      result[[paste0(grp, "_t")]] <- unname(tt$statistic)
      result[[paste0(grp, "_p")]] <- tt$p.value
    } else {
      result[[paste0(grp, "_mean")]] <- result[[paste0(grp, "_t")]] <- result[[paste0(grp, "_p")]] <- NA
    }
  }
  result
})

# Get Neither means for each variable
neither_means <- grp_means %>% filter(facility_choice == "Neither")

# Format table
table2 <- ttest_list %>%
  rowwise() %>%
  mutate(
    Variable = comp_labels[match(variable, comp_vars)],
    Neither = sprintf("%.3f", neither_means[[variable]]),
    BTFP_Only = sprintf("%.3f (%.2f%s)", BTFP_Only_mean, BTFP_Only_t, add_stars(BTFP_Only_p)),
    DW_Only = sprintf("%.3f (%.2f%s)", DW_Only_mean, DW_Only_t, add_stars(DW_Only_p)),
    Both = sprintf("%.3f (%.2f%s)", Both_mean, Both_t, add_stars(Both_p))
  ) %>%
  ungroup() %>%
  select(Variable, Neither, BTFP_Only, DW_Only, Both)

# Add N row (all character now)
n_row <- tibble(Variable = "N", 
                Neither = as.character(grp_n["Neither"]),
                BTFP_Only = as.character(grp_n["BTFP_Only"]),
                DW_Only = as.character(grp_n["DW_Only"]),
                Both = as.character(grp_n["Both"]))

bind_rows(n_row, table2) %>%
  kable(caption = "**Table 2: Bank Characteristics by Facility Choice**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  add_header_above(c(" " = 2, "Mean (T-stat vs Neither)" = 3)) %>%
  footnote(general = "*** p<0.01, ** p<0.05, * p<0.10")
Table 2: Bank Characteristics by Facility Choice
Mean (T-stat vs Neither)
Variable Neither BTFP_Only DW_Only Both
N 2749 880 642 425
Log(Assets) 12.373 13.062 (14.26***) 13.554 (19.22***) 13.966 (20.97***)
MTM Loss (Total) 5.123 6.081 (11.94***) 5.284 (1.69*) 5.980 (8.08***)
MTM Loss (BTFP) 0.536 0.778 (7.83***) 0.561 (0.82) 0.824 (6.79***)
MTM Loss (Non-BTFP) 4.374 4.982 (8.08***) 4.634 (2.88***) 4.972 (6.04***)
Borrowing Subsidy 6.340 7.713 (6.87***) 7.647 (5.57***) 8.343 (8.36***)
Uninsured Leverage 21.429 24.463 (7.01***) 25.477 (7.55***) 28.322 (11.49***)
Run Risk 106.180 144.778 (13.69***) 130.468 (7.51***) 165.936 (14.08***)
Adjusted Equity 6.667 2.345 (-15.45***) 4.522 (-6.94***) 2.613 (-13.17***)
IDCR (100%) 0.122 0.061 (-6.69***) 0.102 (-1.52) 0.082 (-2.84***)
OMO Ratio 9.719 10.703 (2.57**) 7.995 (-4.55***) 10.408 (1.40)
Non-OMO Ratio 72.820 77.881 (9.67***) 78.354 (9.55***) 78.725 (9.23***)
Cash Ratio 10.394 5.691 (-16.76***) 7.499 (-7.64***) 4.877 (-17.34***)
Book Equity 12.083 8.428 (-14.17***) 9.808 (-8.20***) 8.582 (-12.92***)
FHLB Ratio 2.303 3.054 (4.64***) 3.052 (4.05***) 3.725 (6.34***)
Note:
*** p<0.01, ** p<0.05, * p<0.10

3.3 Table 3: Insolvency Distribution by Facility

# ============================================================================
# TABLE 3: INSOLVENCY MEASURES BY FACILITY CHOICE
# ============================================================================

insol_summary <- df %>%
  group_by(facility_choice) %>%
  summarise(
    N = n(),
    `MTM Insolvent (%)` = mean(mtm_insolvent, na.rm = TRUE) * 100,
    `IDCR<0 (50% run)` = mean(insolvent_idcr_50, na.rm = TRUE) * 100,
    `IDCR<0 (100% run)` = mean(insolvent_idcr_100, na.rm = TRUE) * 100,
    `Adj. Equity (Mean)` = mean(adjusted_equity, na.rm = TRUE),
    `IDCR_100 (Mean)` = mean(idcr_100, na.rm = TRUE),
    .groups = "drop"
  )

insol_summary %>%
  mutate(across(where(is.numeric), ~round(., 2))) %>%
  kable(caption = "**Table 3: Insolvency Measures by Facility Choice**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "MTM Insolvent: Adjusted Equity < 0. IDCR: Insured Deposit Coverage Ratio (Jiang et al., 2023)")
Table 3: Insolvency Measures by Facility Choice
facility_choice N MTM Insolvent (%) IDCR<0 (50% run) IDCR<0 (100% run) Adj. Equity (Mean) IDCR_100 (Mean)
Neither 2749 15.12 4.13 25.74 6.67 0.12
BTFP_Only 880 29.43 4.09 32.05 2.35 0.06
DW_Only 642 14.17 5.76 30.22 4.52 0.10
Both 425 23.76 4.47 31.06 2.61 0.08
Note:
MTM Insolvent: Adjusted Equity < 0. IDCR: Insured Deposit Coverage Ratio (Jiang et al., 2023)

4 Descriptive Figures

# ============================================================================
# FIGURE 1: DAILY BORROWING TIMELINE
# ============================================================================

btfp_daily <- btfp_filt %>%
  filter(btfp_loan_date >= "2023-03-01", btfp_loan_date <= "2024-03-15") %>%
  group_by(date = btfp_loan_date) %>%
  summarise(amt = sum(btfp_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "BTFP")

dw_daily <- dw_filt %>%
  filter(dw_loan_date >= "2023-03-01", dw_loan_date <= "2024-03-15") %>%
  group_by(date = dw_loan_date) %>%
  summarise(amt = sum(dw_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "DW")

daily_all <- bind_rows(btfp_daily, dw_daily)

fig1 <- ggplot() +
  annotate("rect", xmin = periods$start_date, xmax = periods$end_date,
           ymin = -Inf, ymax = Inf,
           fill = c("#FFE5E5", "#FC9272", "#A1D99B", "#9ECAE1", "#DADAEB"),
           alpha = 0.4) +
  geom_col(data = daily_all, aes(x = date, y = amt, fill = facility),
           position = "dodge", width = 1) +
  scale_fill_manual(values = c("BTFP" = COLORS$btfp, "DW" = COLORS$dw)) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
  scale_y_continuous(labels = dollar_format(suffix = "B")) +
  labs(title = "Figure 1: Daily Federal Reserve Lending During the 2023 Banking Crisis",
       subtitle = "Periods: Pre-BTFP | Acute | Post-Acute | Arbitrage | Wind-down",
       x = NULL, y = "Daily Borrowing ($B)", fill = "Facility") +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

print(fig1)

# ============================================================================
# FIGURE 2: BORROWER CHARACTERISTICS
# ============================================================================

p1 <- ggplot(df, aes(x = facility_choice, y = mtm_btfp, fill = facility_choice)) +
  geom_boxplot(outlier.shape = NA) +
  coord_cartesian(ylim = c(0, quantile(df$mtm_btfp, 0.95, na.rm = TRUE))) +
  scale_fill_manual(values = c("Neither" = COLORS$neither, "BTFP_Only" = COLORS$btfp,
                                "DW_Only" = COLORS$dw, "Both" = COLORS$both)) +
  labs(title = "A: MTM Loss (BTFP-Eligible)", y = "%") +
  theme_pub() + theme(legend.position = "none", axis.title.x = element_blank())

p2 <- ggplot(df, aes(x = facility_choice, y = mtm_other, fill = facility_choice)) +
  geom_boxplot(outlier.shape = NA) +
  coord_cartesian(ylim = c(0, quantile(df$mtm_other, 0.95, na.rm = TRUE))) +
  scale_fill_manual(values = c("Neither" = COLORS$neither, "BTFP_Only" = COLORS$btfp,
                                "DW_Only" = COLORS$dw, "Both" = COLORS$both)) +
  labs(title = "B: MTM Loss (Non-BTFP)", y = "%") +
  theme_pub() + theme(legend.position = "none", axis.title.x = element_blank())

p3 <- ggplot(df, aes(x = facility_choice, y = uninsured_lev, fill = facility_choice)) +
  geom_boxplot(outlier.shape = NA) +
  coord_cartesian(ylim = c(0, quantile(df$uninsured_lev, 0.95, na.rm = TRUE))) +
  scale_fill_manual(values = c("Neither" = COLORS$neither, "BTFP_Only" = COLORS$btfp,
                                "DW_Only" = COLORS$dw, "Both" = COLORS$both)) +
  labs(title = "C: Uninsured Leverage", y = "%") +
  theme_pub() + theme(legend.position = "none", axis.title.x = element_blank())

p4 <- ggplot(df, aes(x = facility_choice, y = adjusted_equity, fill = facility_choice)) +
  geom_boxplot(outlier.shape = NA) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  coord_cartesian(ylim = quantile(df$adjusted_equity, c(0.02, 0.98), na.rm = TRUE)) +
  scale_fill_manual(values = c("Neither" = COLORS$neither, "BTFP_Only" = COLORS$btfp,
                                "DW_Only" = COLORS$dw, "Both" = COLORS$both)) +
  labs(title = "D: Adjusted Equity (Book - MTM)", y = "%") +
  theme_pub() + theme(legend.position = "none", axis.title.x = element_blank())

(p1 + p2) / (p3 + p4) +
  plot_annotation(title = "Figure 2: Bank Characteristics by Facility Choice",
                  subtitle = "BTFP users: Higher MTM on eligible securities | Both users: Most vulnerable")

5 Main Regression Analysis

5.1 Global Model Settings

# ============================================================================
# GLOBAL MODEL SETTINGS
# ============================================================================

# Standard controls (ALL WINSORIZED)
CONTROLS <- "ln_assets + cash_ratio + securities_ratio + loan_to_deposit + book_equity_ratio + wholesale + fhlb_ratio + roa"

# Coefficient labels
COEF_MAP <- c(
  "mtm_total" = "MTM Loss (Total)",
  "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
  "mtm_other" = "MTM Loss (Non-BTFP)",
  "uninsured_lev" = "Uninsured Leverage",
  "borrowing_subsidy" = "Borrowing Subsidy",
  "run_risk" = "Run Risk (MTM Ă— Uninsured)",
  "I(mtm_btfp * uninsured_lev)" = "MTM(BTFP) Ă— Uninsured",
  "I(mtm_total * uninsured_lev)" = "MTM Ă— Uninsured",
  "adjusted_equity" = "Adjusted Equity",
  "mtm_insolvent" = "MTM Insolvent (Dummy)",
  "idcr_100" = "IDCR (100% run)",
  "insolvent_idcr_100" = "Insolvent (IDCR<0)",
  "prior_dw" = "Prior DW User",
  "omo_ratio" = "OMO-Eligible Ratio",
  "non_omo_ratio" = "Non-OMO Ratio",
  "maxed_out" = "Maxed Out (>90%)",
  "high_uninsured" = "High Uninsured (Dummy)",
  "high_mtm_btfp" = "High MTM-BTFP (Dummy)",
  "ln_assets" = "Log(Assets)",
  "cash_ratio" = "Cash Ratio",
  "securities_ratio" = "Securities Ratio",
  "loan_to_deposit" = "Loan/Deposit",
  "book_equity_ratio" = "Book Equity",
  "wholesale" = "Wholesale Funding",
  "fhlb_ratio" = "FHLB Ratio",
  "roa" = "ROA"
)

cat("=== MODEL SETTINGS ===\n")

=== MODEL SETTINGS ===

cat("Controls:", CONTROLS, "\n")

Controls: ln_assets + cash_ratio + securities_ratio + loan_to_deposit + book_equity_ratio + wholesale + fhlb_ratio + roa

cat("Sample size:", nrow(df), "\n")

Sample size: 4696

5.2 Table 4: Main Extensive Margin - Par Valuation Test

Causal Test: If par valuation drives BTFP selection, MTM(BTFP-eligible) should predict BTFP; MTM(Non-eligible) should NOT.

# ============================================================================
# TABLE 4: MAIN EXTENSIVE MARGIN - PAR VALUATION TEST
# ============================================================================

f1 <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f2 <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f3 <- as.formula(paste("dw ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f4 <- as.formula(paste("any_fed ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f5 <- as.formula(paste("both ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))

m4_1 <- feols(f1, data = df, vcov = "hetero")
m4_2 <- feols(f2, data = df, vcov = "hetero")
m4_3 <- feols(f3, data = df, vcov = "hetero")
m4_4 <- feols(f4, data = df, vcov = "hetero")
m4_5 <- feols(f5, data = df, vcov = "hetero")

modelsummary(
  list("(1) BTFP" = m4_1, "(2) BTFP Int" = m4_2, "(3) DW" = m4_3,
       "(4) Any Fed" = m4_4, "(5) Both" = m4_5),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 4: Main Extensive Margin - Par Valuation Test",
  notes = list("Robust SE. All variables winsorized.",
               "KEY TEST: MTM(BTFP-Eligible) should predict BTFP but NOT DW.")
)
Table 4: Main Extensive Margin - Par Valuation Test
(1) BTFP (2) BTFP Int (3) DW (4) Any Fed (5) Both
* p < 0.1, ** p < 0.05, *** p < 0.01
Robust SE. All variables winsorized.
KEY TEST: MTM(BTFP-Eligible) should predict BTFP but NOT DW.
MTM Loss (BTFP-Eligible) 0.044*** 0.065*** 0.009 0.055*** 0.009
(0.011) (0.019) (0.009) (0.020) (0.013)
MTM Loss (Non-BTFP) 0.010*** 0.010*** 0.001 0.009** 0.003
(0.003) (0.003) (0.003) (0.004) (0.002)
Uninsured Leverage 0.002*** 0.003*** 0.001 0.002** 0.001**
(0.001) (0.001) (0.001) (0.001) (0.000)
MTM(BTFP) Ă— Uninsured -0.001 -0.001 0.000
(0.001) (0.001) (0.001)
Log(Assets) 0.054*** 0.054*** 0.094*** 0.104*** 0.043***
(0.006) (0.006) (0.005) (0.006) (0.004)
Cash Ratio -0.005*** -0.005*** -0.000 -0.004*** -0.001***
(0.001) (0.001) (0.001) (0.001) (0.000)
Securities Ratio 0.002** 0.002* 0.000 0.002* 0.000
(0.001) (0.001) (0.001) (0.001) (0.000)
Loan/Deposit -0.001** -0.001** 0.001 0.000 -0.001*
(0.001) (0.001) (0.001) (0.001) (0.000)
Book Equity -0.002*** -0.002*** 0.000 -0.002*** 0.001
(0.001) (0.001) (0.001) (0.001) (0.000)
Wholesale Funding 0.009*** 0.009*** 0.002 0.005 0.006**
(0.003) (0.003) (0.003) (0.003) (0.002)
FHLB Ratio 0.007*** 0.007*** 0.001 0.005** 0.004***
(0.002) (0.002) (0.002) (0.002) (0.001)
ROA -0.000 -0.000 0.002 0.006 -0.004
(0.007) (0.007) (0.007) (0.008) (0.004)
Num.Obs. 4678 4678 4678 4678 4678
R2 0.107 0.107 0.120 0.156 0.077

5.3 Table 5: Borrowing Subsidy Specification

# ============================================================================
# TABLE 5: BORROWING SUBSIDY SPECIFICATION
# ============================================================================

f_sub1 <- as.formula(paste("btfp ~ borrowing_subsidy + uninsured_lev +", CONTROLS))
f_sub2 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))
f_sub3 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev + I(borrowing_subsidy * uninsured_lev) +", CONTROLS))
f_sub4 <- as.formula(paste("dw ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))

m5_1 <- feols(f_sub1, data = df, vcov = "hetero")
m5_2 <- feols(f_sub2, data = df, vcov = "hetero")
m5_3 <- feols(f_sub3, data = df, vcov = "hetero")
m5_4 <- feols(f_sub4, data = df, vcov = "hetero")

modelsummary(
  list("(1) Subsidy" = m5_1, "(2) + Non-BTFP" = m5_2,
       "(3) Interaction" = m5_3, "(4) DW" = m5_4),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 5: Borrowing Subsidy and Facility Selection",
  notes = "Borrowing Subsidy = MTM Loss Rate on OMO-Eligible (par valuation benefit)"
)
Table 5: Borrowing Subsidy and Facility Selection
(1) Subsidy (2) + Non-BTFP (3) Interaction (4) DW
* p < 0.1, ** p < 0.05, *** p < 0.01
Borrowing Subsidy = MTM Loss Rate on OMO-Eligible (par valuation benefit)
MTM Loss (Non-BTFP) 0.006* 0.006* -0.001
(0.004) (0.004) (0.003)
Uninsured Leverage 0.002** 0.002** 0.002** 0.001
(0.001) (0.001) (0.001) (0.001)
Borrowing Subsidy 0.003* 0.002 0.003 0.002*
(0.001) (0.001) (0.003) (0.001)
Log(Assets) 0.058*** 0.057*** 0.057*** 0.092***
(0.006) (0.006) (0.006) (0.006)
Cash Ratio -0.005*** -0.005*** -0.005*** -0.001
(0.001) (0.001) (0.001) (0.001)
Securities Ratio 0.004*** 0.004*** 0.004*** -0.000
(0.001) (0.001) (0.001) (0.001)
Loan/Deposit -0.000 -0.000 -0.000 0.001
(0.001) (0.001) (0.001) (0.001)
Book Equity -0.003*** -0.003*** -0.003*** 0.000
(0.001) (0.001) (0.001) (0.001)
Wholesale Funding 0.007* 0.007** 0.007** 0.002
(0.003) (0.003) (0.003) (0.003)
FHLB Ratio 0.008*** 0.008*** 0.008*** 0.000
(0.002) (0.002) (0.002) (0.002)
ROA -0.003 -0.002 -0.002 0.001
(0.008) (0.008) (0.008) (0.008)
Num.Obs. 4282 4282 4282 4282
R2 0.097 0.097 0.097 0.120

5.4 Table 6: Placebo Test - Pre-BTFP DW Usage

Causal Test: If MTM(BTFP-eligible) matters because of par valuation, it should NOT predict DW usage BEFORE BTFP existed.

# ============================================================================
# TABLE 6: PLACEBO TEST - PRE-BTFP DW USAGE
# ============================================================================

f_pla1 <- as.formula(paste("dw_pre ~ mtm_total + uninsured_lev +", CONTROLS))
f_pla2 <- as.formula(paste("dw_pre ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f_pla3 <- as.formula(paste("dw_pre ~ mtm_total + uninsured_lev + I(mtm_total * uninsured_lev) +", CONTROLS))

m6_1 <- feols(f_pla1, data = df, vcov = "hetero")
m6_2 <- feols(f_pla2, data = df, vcov = "hetero")
m6_3 <- feols(f_pla3, data = df, vcov = "hetero")

modelsummary(
  list("(1) Placebo: Total" = m6_1, "(2) Placebo: Split" = m6_2,
       "(3) Placebo: Int" = m6_3, "(4) Main: BTFP" = m4_2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 6: Placebo Test - Pre-BTFP DW vs Post-BTFP BTFP",
  notes = list("Placebo: DW usage Mar 1-12, 2023 (before BTFP existed).",
               "IDENTIFICATION: MTM(BTFP) should matter MORE for BTFP than for pre-BTFP DW.")
)
Table 6: Placebo Test - Pre-BTFP DW vs Post-BTFP BTFP
(1) Placebo: Total (2) Placebo: Split (3) Placebo: Int (4) Main: BTFP
* p < 0.1, ** p < 0.05, *** p < 0.01
Placebo: DW usage Mar 1-12, 2023 (before BTFP existed).
IDENTIFICATION: MTM(BTFP) should matter MORE for BTFP than for pre-BTFP DW.
MTM Loss (Total) 0.002 -0.006
(0.003) (0.005)
MTM Loss (BTFP-Eligible) 0.010 0.065***
(0.009) (0.019)
MTM Loss (Non-BTFP) 0.004 0.010***
(0.003) (0.003)
Uninsured Leverage -0.001 -0.001 -0.003** 0.003***
(0.001) (0.001) (0.001) (0.001)
MTM(BTFP) Ă— Uninsured -0.001
(0.001)
MTM Ă— Uninsured 0.000*
(0.000)
Log(Assets) 0.141*** 0.139*** 0.140*** 0.054***
(0.005) (0.005) (0.005) (0.006)
Cash Ratio 0.000 0.000 0.000 -0.005***
(0.001) (0.001) (0.001) (0.001)
Securities Ratio 0.000 0.000 0.001 0.002*
(0.001) (0.001) (0.001) (0.001)
Loan/Deposit 0.000 0.000 0.000 -0.001**
(0.001) (0.001) (0.001) (0.001)
Book Equity 0.000 0.001 0.000 -0.002***
(0.001) (0.001) (0.001) (0.001)
Wholesale Funding 0.004 0.004 0.004 0.009***
(0.003) (0.003) (0.003) (0.003)
FHLB Ratio 0.003** 0.003* 0.003* 0.007***
(0.002) (0.002) (0.002) (0.002)
ROA -0.008 -0.007 -0.009 -0.000
(0.007) (0.007) (0.007) (0.007)
Num.Obs. 4678 4678 4678 4678
R2 0.200 0.200 0.200 0.107

6 Insolvency Analysis

6.1 Table 7: Insolvency Measures and Facility Choice

# ============================================================================
# TABLE 7: INSOLVENCY MEASURES AND FACILITY CHOICE
# ============================================================================

f_ins1 <- as.formula(paste("btfp ~ adjusted_equity + uninsured_lev +", CONTROLS))
f_ins2 <- as.formula(paste("btfp ~ mtm_insolvent + uninsured_lev +", CONTROLS))
f_ins3 <- as.formula(paste("btfp ~ idcr_100 + uninsured_lev +", CONTROLS))
f_ins4 <- as.formula(paste("btfp ~ insolvent_idcr_100 + uninsured_lev +", CONTROLS))

m7_1 <- feols(f_ins1, data = df, vcov = "hetero")
m7_2 <- feols(f_ins2, data = df, vcov = "hetero")
m7_3 <- feols(f_ins3, data = df, vcov = "hetero")
m7_4 <- feols(f_ins4, data = df, vcov = "hetero")

modelsummary(
  list("(1) Adj Equity" = m7_1, "(2) MTM Insolvent" = m7_2,
       "(3) IDCR" = m7_3, "(4) IDCR Insolvent" = m7_4),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 7: Insolvency Measures and BTFP Selection",
  notes = list("Adjusted Equity = Book Equity - MTM Losses (Jiang et al. 2023).",
               "MTM Insolvent = 1 if Adjusted Equity < 0.")
)
Table 7: Insolvency Measures and BTFP Selection
(1) Adj Equity (2) MTM Insolvent (3) IDCR (4) IDCR Insolvent
* p < 0.1, ** p < 0.05, *** p < 0.01
Adjusted Equity = Book Equity - MTM Losses (Jiang et al. 2023).
MTM Insolvent = 1 if Adjusted Equity < 0.
Uninsured Leverage 0.002*** 0.002*** 0.002*** 0.001**
(0.001) (0.001) (0.001) (0.001)
Adjusted Equity -0.017***
(0.003)
MTM Insolvent (Dummy) 0.094***
(0.019)
IDCR (100% run) -0.055**
(0.027)
Insolvent (IDCR<0) 0.035**
(0.015)
Log(Assets) 0.058*** 0.060*** 0.059*** 0.059***
(0.005) (0.006) (0.006) (0.006)
Cash Ratio -0.005*** -0.005*** -0.005*** -0.005***
(0.001) (0.001) (0.001) (0.001)
Securities Ratio 0.002** 0.003*** 0.004*** 0.004***
(0.001) (0.001) (0.001) (0.001)
Loan/Deposit -0.001** -0.000 -0.000 0.000
(0.001) (0.001) (0.001) (0.001)
Book Equity 0.016*** -0.001* -0.002** -0.003***
(0.003) (0.001) (0.001) (0.001)
Wholesale Funding 0.010*** 0.008** 0.009*** 0.008**
(0.003) (0.003) (0.003) (0.003)
FHLB Ratio 0.007*** 0.006*** 0.007*** 0.006***
(0.002) (0.002) (0.002) (0.002)
ROA -0.001 -0.007 -0.006 -0.007
(0.007) (0.007) (0.008) (0.008)
Num.Obs. 4678 4678 4632 4632
R2 0.108 0.107 0.102 0.102

6.2 Table 8: Insolvency and Any Fed Access

# ============================================================================
# TABLE 8: INSOLVENCY AND ANY FED ACCESS
# ============================================================================
# Using only one explanatory each time

f_any1 <- as.formula(paste("any_fed ~ mtm_insolvent +", CONTROLS))
f_any2 <- as.formula(paste("any_fed ~ idcr_100 + ", CONTROLS))
f_any3 <- as.formula(paste("any_fed ~ adjusted_equity + ", CONTROLS))
# Fixed hidden characters/spacing in the paste string below
f_any4 <- as.formula(paste("any_fed ~ run_risk +", CONTROLS)) 

m8_1 <- feols(f_any1, data = df, vcov = "hetero")
m8_2 <- feols(f_any2, data = df, vcov = "hetero")
m8_3 <- feols(f_any3, data = df, vcov = "hetero")
# Fixed: Changed f_any3 to f_any4
m8_4 <- feols(f_any4, data = df, vcov = "hetero") 

modelsummary(
  list(
    "(1) MTM Insolvent" = m8_1, 
    "(2) IDCR Insolvent" = m8_2, 
    "(3) Adj. Insolvent" = m8_3, 
    "(4) Run Risk" = m8_4
  ),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 8: Did Vulnerable Banks Access ANY Fed Facility?",
  # FIXED: Replaced special 'Ă—' with standard 'x'
  notes = "Run Risk = MTM x Uninsured (continuous interaction)"
)
Table 8: Did Vulnerable Banks Access ANY Fed Facility?
(1) MTM Insolvent (2) IDCR Insolvent (3) Adj. Insolvent (4) Run Risk
* p < 0.1, ** p < 0.05, *** p < 0.01
Run Risk = MTM x Uninsured (continuous interaction)
Run Risk (MTM Ă— Uninsured) 0.001***
(0.000)
Adjusted Equity -0.013***
(0.003)
MTM Insolvent (Dummy) 0.080***
(0.020)
IDCR (100% run) -0.037
(0.029)
Log(Assets) 0.113*** 0.113*** 0.113*** 0.100***
(0.005) (0.005) (0.005) (0.005)
Cash Ratio -0.003*** -0.003*** -0.003*** -0.003***
(0.001) (0.001) (0.001) (0.001)
Securities Ratio 0.003*** 0.003*** 0.002** 0.002**
(0.001) (0.001) (0.001) (0.001)
Loan/Deposit 0.001 0.001 0.000 0.000
(0.001) (0.001) (0.001) (0.001)
Book Equity -0.002*** -0.004*** 0.010*** -0.002***
(0.001) (0.001) (0.003) (0.001)
Wholesale Funding 0.004 0.004 0.005 0.006
(0.003) (0.004) (0.003) (0.003)
FHLB Ratio 0.004* 0.004* 0.004* 0.005**
(0.002) (0.002) (0.002) (0.002)
ROA 0.002 0.002 0.006 0.002
(0.007) (0.009) (0.008) (0.007)
Num.Obs. 4678 4632 4678 4678
R2 0.155 0.149 0.155 0.159

7 Temporal Dynamics

7.1 Table 9: Period-Specific Entry

# ============================================================================
# TABLE 9: TEMPORAL DYNAMICS
# ============================================================================

f_acute <- as.formula(paste("btfp_acute ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f_post <- as.formula(paste("btfp_post ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))
f_arb <- as.formula(paste("btfp_arb ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))

m9_acute <- feols(f_acute, data = df, vcov = "hetero")
m9_post <- feols(f_post, data = df, vcov = "hetero")
m9_arb <- feols(f_arb, data = df, vcov = "hetero")

modelsummary(
  list("(1) Acute" = m9_acute, "(2) Post-Acute" = m9_post, "(3) Arbitrage" = m9_arb),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 9: BTFP Entry by Crisis Phase",
  notes = "Acute: Run-driven | Post-Acute: Par valuation | Arbitrage: Rate advantage"
)
Table 9: BTFP Entry by Crisis Phase
(1) Acute (2) Post-Acute (3) Arbitrage
* p < 0.1, ** p < 0.05, *** p < 0.01
Acute: Run-driven | Post-Acute: Par valuation | Arbitrage: Rate advantage
MTM Loss (BTFP-Eligible) -0.005 0.046*** 0.024**
(0.013) (0.015) (0.009)
MTM Loss (Non-BTFP) 0.003 0.004 0.003*
(0.002) (0.002) (0.002)
Uninsured Leverage 0.001 0.001** 0.001
(0.001) (0.000) (0.000)
MTM(BTFP) Ă— Uninsured 0.001* -0.001* -0.001***
(0.001) (0.001) (0.000)
Log(Assets) 0.031*** 0.010** 0.014***
(0.004) (0.004) (0.003)
Cash Ratio -0.002*** -0.002*** -0.001**
(0.000) (0.001) (0.000)
Securities Ratio 0.000 0.001** -0.000
(0.000) (0.001) (0.000)
Loan/Deposit -0.001*** 0.000 -0.000
(0.000) (0.000) (0.000)
Book Equity -0.001 -0.001*** 0.000
(0.000) (0.000) (0.000)
Wholesale Funding 0.011*** -0.001 -0.001
(0.003) (0.002) (0.001)
FHLB Ratio 0.007*** -0.001 0.001
(0.001) (0.001) (0.001)
ROA 0.003 0.004 -0.008**
(0.004) (0.005) (0.003)
Num.Obs. 4678 4678 4678
R2 0.070 0.026 0.015

7.2 Table 10: Coefficient Equality Tests

# ============================================================================
# TABLE 10: COEFFICIENT EQUALITY TESTS
# ============================================================================

get_coef <- function(model, var) {
  list(est = coef(model)[var], se = sqrt(vcov(model)[var, var]))
}

# MTM(BTFP) across periods
c_acute <- get_coef(m9_acute, "mtm_btfp")
c_post <- get_coef(m9_post, "mtm_btfp")
c_arb <- get_coef(m9_arb, "mtm_btfp")

# Z-tests
z_ap <- (c_acute$est - c_post$est) / sqrt(c_acute$se^2 + c_post$se^2)
z_pa <- (c_post$est - c_arb$est) / sqrt(c_post$se^2 + c_arb$se^2)
z_aa <- (c_acute$est - c_arb$est) / sqrt(c_acute$se^2 + c_arb$se^2)

tibble(
  Comparison = c("Acute vs Post-Acute", "Post-Acute vs Arbitrage", "Acute vs Arbitrage"),
  `Coef 1` = c(c_acute$est, c_post$est, c_acute$est),
  `Coef 2` = c(c_post$est, c_arb$est, c_arb$est),
  Difference = c(c_acute$est - c_post$est, c_post$est - c_arb$est, c_acute$est - c_arb$est),
  `Z-stat` = c(z_ap, z_pa, z_aa),
  `P-value` = 2 * (1 - pnorm(abs(c(z_ap, z_pa, z_aa))))
) %>%
  mutate(across(c(`Coef 1`, `Coef 2`, Difference), ~round(., 4)),
         `Z-stat` = round(`Z-stat`, 2),
         `P-value` = round(`P-value`, 3),
         Sig = ifelse(`P-value` < 0.05, "Yes", "No")) %>%
  kable(caption = "**Table 10: Coefficient Equality Tests - MTM(BTFP) Across Periods**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 10: Coefficient Equality Tests - MTM(BTFP) Across Periods
Comparison Coef 1 Coef 2 Difference Z-stat P-value Sig
Acute vs Post-Acute -0.0051 0.0462 -0.0513 -2.62 0.009 Yes
Post-Acute vs Arbitrage 0.0462 0.0237 0.0225 1.27 0.203 No
Acute vs Arbitrage -0.0051 0.0237 -0.0289 -1.83 0.067 No

7.3 Figure 3: Coefficient Evolution

coef_evol <- bind_rows(
  tidy(m9_acute, conf.int = TRUE) %>% mutate(period = "Acute"),
  tidy(m9_post, conf.int = TRUE) %>% mutate(period = "Post-Acute"),
  tidy(m9_arb, conf.int = TRUE) %>% mutate(period = "Arbitrage")
) %>%
  filter(term %in% c("mtm_btfp", "mtm_other", "uninsured_lev")) %>%
  mutate(
    term = case_when(
      term == "mtm_btfp" ~ "MTM (BTFP-Eligible)",
      term == "mtm_other" ~ "MTM (Non-BTFP)",
      term == "uninsured_lev" ~ "Uninsured Leverage"
    ),
    period = factor(period, levels = c("Acute", "Post-Acute", "Arbitrage"))
  )

ggplot(coef_evol, aes(x = period, y = estimate, color = term, group = term)) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  geom_line(size = 1) +
  geom_point(size = 3) +
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = 0.1) +
  facet_wrap(~term, scales = "free_y") +
  labs(title = "Figure 3: Coefficient Evolution Across Crisis Phases",
       subtitle = "How borrowing determinants change over time",
       x = "Period", y = "Coefficient (95% CI)") +
  theme_pub() + theme(legend.position = "none")

8 Intensive Margin

8.1 Table 11: Borrowing Amount Conditional on Participation

# ============================================================================
# TABLE 11: INTENSIVE MARGIN
# ============================================================================

df_btfp <- df %>% filter(btfp == 1, !is.na(btfp_pct))

f_int1 <- as.formula(paste("btfp_pct ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f_int2 <- as.formula(paste("btfp_pct ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))
f_int3 <- as.formula(paste("btfp_pct ~ borrowing_subsidy + mtm_other + uninsured_lev + omo_ratio +", CONTROLS))

m11_1 <- feols(f_int1, data = df_btfp, vcov = "hetero")
m11_2 <- feols(f_int2, data = df_btfp, vcov = "hetero")
m11_3 <- feols(f_int3, data = df_btfp, vcov = "hetero")

# Utilization
f_util <- as.formula(paste("btfp_util ~ borrowing_subsidy + uninsured_lev + omo_ratio +", CONTROLS))
m11_util <- feols(f_util, data = df_btfp, vcov = "hetero")

modelsummary(
  list("(1) Amount: MTM" = m11_1, "(2) Amount: Subsidy" = m11_2,
       "(3) Amount: Full" = m11_3, "(4) Utilization" = m11_util),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 11: Intensive Margin - BTFP Borrowing Amount",
  notes = list("Sample: BTFP borrowers only.",
               "Cols 1-3: Amount as % of assets. Col 4: Utilization = Amount/OMO-Eligible.")
)
Table 11: Intensive Margin - BTFP Borrowing Amount
(1) Amount: MTM (2) Amount: Subsidy (3) Amount: Full (4) Utilization
* p < 0.1, ** p < 0.05, *** p < 0.01
Sample: BTFP borrowers only.
Cols 1-3: Amount as % of assets. Col 4: Utilization = Amount/OMO-Eligible.
MTM Loss (BTFP-Eligible) 0.336
(0.857)
MTM Loss (Non-BTFP) -0.580** -0.570** -0.319
(0.290) (0.276) (0.323)
Uninsured Leverage 0.122* 0.121* 0.133** 0.023
(0.063) (0.063) (0.063) (0.036)
Borrowing Subsidy -0.115 -0.111 0.179
(0.113) (0.114) (0.129)
OMO-Eligible Ratio 0.130 -0.350***
(0.087) (0.050)
Log(Assets) -0.094 -0.021 -0.177 -0.161
(0.483) (0.494) (0.478) (0.347)
Cash Ratio -0.487*** -0.522*** -0.527*** -0.101
(0.167) (0.175) (0.176) (0.119)
Securities Ratio 0.004 -0.042 -0.093 0.100
(0.175) (0.177) (0.187) (0.104)
Loan/Deposit -0.265 -0.304* -0.308* -0.003
(0.162) (0.165) (0.164) (0.090)
Book Equity 0.607** 0.633** 0.629** 0.165
(0.260) (0.268) (0.268) (0.123)
Wholesale Funding 0.575** 0.609** 0.611** -0.037
(0.287) (0.290) (0.290) (0.182)
FHLB Ratio 0.913*** 0.967*** 0.990*** 0.246**
(0.253) (0.259) (0.256) (0.111)
ROA 0.683 0.684 0.970 0.028
(1.174) (1.331) (1.333) (0.528)
Num.Obs. 1305 1259 1259 1259
R2 0.076 0.074 0.077 0.076

8.2 Table 12: Facility Selection Among Fed Borrowers

# ============================================================================
# TABLE 12: BTFP SELECTION CONDITIONAL ON FED BORROWING
# ============================================================================

df_fed <- df %>% filter(any_fed == 1)

f_sel1 <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev +", CONTROLS))
f_sel2 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev +", CONTROLS))
f_sel3 <- as.formula(paste("btfp ~ borrowing_subsidy + mtm_other + uninsured_lev + prior_dw + omo_ratio +", CONTROLS))

m12_1 <- feols(f_sel1, data = df_fed, vcov = "hetero")
m12_2 <- feols(f_sel2, data = df_fed, vcov = "hetero")
m12_3 <- feols(f_sel3, data = df_fed, vcov = "hetero")

modelsummary(
  list("(1) Split MTM" = m12_1, "(2) Subsidy" = m12_2, "(3) Full" = m12_3),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 12: BTFP Selection Among Fed Borrowers",
  notes = "Sample: Banks that borrowed from BTFP or DW."
)
Table 12: BTFP Selection Among Fed Borrowers
(1) Split MTM (2) Subsidy (3) Full
* p < 0.1, ** p < 0.05, *** p < 0.01
Sample: Banks that borrowed from BTFP or DW.
MTM Loss (BTFP-Eligible) 0.032**
(0.014)
MTM Loss (Non-BTFP) 0.005 0.004 0.010
(0.006) (0.006) (0.007)
Uninsured Leverage 0.003*** 0.002** 0.002*
(0.001) (0.001) (0.001)
Borrowing Subsidy -0.000 -0.000
(0.002) (0.002)
Prior DW User -0.264***
(0.022)
OMO-Eligible Ratio 0.003*
(0.001)
Log(Assets) -0.031*** -0.024*** 0.005
(0.009) (0.009) (0.010)
Cash Ratio -0.008*** -0.006* -0.006*
(0.003) (0.003) (0.003)
Securities Ratio 0.006** 0.009*** 0.006**
(0.003) (0.003) (0.003)
Loan/Deposit 0.001 0.003 0.002
(0.002) (0.003) (0.002)
Book Equity -0.009** -0.009** -0.009**
(0.004) (0.004) (0.004)
Wholesale Funding 0.006 0.004 0.008
(0.005) (0.005) (0.005)
FHLB Ratio 0.004 0.005 0.007*
(0.004) (0.004) (0.004)
ROA -0.006 -0.015 -0.010
(0.018) (0.018) (0.018)
Num.Obs. 1947 1850 1850
R2 0.086 0.076 0.148

9 Heterogeneity Analysis

9.1 Table 13: Heterogeneity by Bank Size

# ============================================================================
# TABLE 13: HETEROGENEITY BY SIZE
# ============================================================================

df_small <- df %>% filter(size_cat == "small")
df_large <- df %>% filter(size_cat == "large")

f_het <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))

m13_small <- feols(f_het, data = df_small, vcov = "hetero")
m13_large <- feols(f_het, data = df_large, vcov = "hetero")

modelsummary(
  list("(1) Small Banks" = m13_small, "(2) Large Banks" = m13_large),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 13: Heterogeneity by Bank Size"
)
Table 13: Heterogeneity by Bank Size
(1) Small Banks (2) Large Banks
* p < 0.1, ** p < 0.05, *** p < 0.01
MTM Loss (BTFP-Eligible) 0.079*** -0.003
(0.021) (0.057)
MTM Loss (Non-BTFP) 0.006* 0.022**
(0.003) (0.010)
Uninsured Leverage 0.002** 0.003
(0.001) (0.002)
MTM(BTFP) Ă— Uninsured -0.002* 0.001
(0.001) (0.002)
Log(Assets) 0.082*** 0.002
(0.008) (0.020)
Cash Ratio -0.005*** -0.005
(0.001) (0.004)
Securities Ratio 0.001 0.007*
(0.001) (0.004)
Loan/Deposit -0.001*** 0.002
(0.001) (0.003)
Book Equity -0.002** -0.000
(0.001) (0.004)
Wholesale Funding 0.012*** -0.009
(0.004) (0.009)
FHLB Ratio 0.006*** 0.013***
(0.002) (0.005)
ROA -0.002 0.012
(0.007) (0.030)
Num.Obs. 3947 731
R2 0.100 0.088

10 Multi-Facility Use

10.1 Table 14: Why Use Both Facilities?

# ============================================================================
# TABLE 14: DETERMINANTS OF USING BOTH
# ============================================================================

f_both1 <- as.formula(paste("both ~ mtm_btfp + mtm_other + omo_ratio + non_omo_ratio + uninsured_lev +", CONTROLS))
f_both2 <- as.formula(paste("both ~ maxed_out + non_omo_ratio + uninsured_lev +", CONTROLS))
f_both3 <- as.formula(paste("both ~ maxed_out + borrowing_subsidy + non_omo_ratio + uninsured_lev + prior_dw +", CONTROLS))

m14_1 <- feols(f_both1, data = df_fed, vcov = "hetero")
m14_2 <- feols(f_both2, data = df_fed, vcov = "hetero")
m14_3 <- feols(f_both3, data = df_fed, vcov = "hetero")

modelsummary(
  list("(1) Collateral" = m14_1, "(2) Maxed Out" = m14_2, "(3) Full" = m14_3),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 14: Determinants of Using Both Facilities",
  notes = "Sample: Fed borrowers. Maxed Out = BTFP utilization > 90%."
)
Table 14: Determinants of Using Both Facilities
(1) Collateral (2) Maxed Out (3) Full
* p < 0.1, ** p < 0.05, *** p < 0.01
Sample: Fed borrowers. Maxed Out = BTFP utilization > 90%.
MTM Loss (BTFP-Eligible) 0.004
(0.019)
MTM Loss (Non-BTFP) 0.003
(0.006)
Uninsured Leverage 0.003*** 0.002*** 0.003***
(0.001) (0.001) (0.001)
Borrowing Subsidy -0.001
(0.002)
Prior DW User 0.180***
(0.020)
OMO-Eligible Ratio 0.003
(0.005)
Non-OMO Ratio 0.002 -0.002** -0.003***
(0.004) (0.001) (0.001)
Maxed Out (>90%) 0.141*** 0.162***
(0.022) (0.021)
Log(Assets) 0.049*** 0.052*** 0.033***
(0.008) (0.008) (0.009)
Cash Ratio -0.003 -0.003 -0.003
(0.003) (0.002) (0.003)
Securities Ratio -0.001 0.002 0.003
(0.004) (0.002) (0.002)
Loan/Deposit -0.001 0.001 0.003
(0.003) (0.002) (0.002)
Book Equity -0.002 -0.005 -0.006*
(0.004) (0.003) (0.003)
Wholesale Funding 0.010* 0.006 0.003
(0.005) (0.005) (0.005)
FHLB Ratio 0.006 0.002 -0.000
(0.004) (0.003) (0.003)
ROA -0.004 -0.004 -0.010
(0.017) (0.016) (0.016)
Num.Obs. 1947 1947 1850
R2 0.060 0.082 0.125

10.2 Table 15: Sequencing Analysis

# ============================================================================
# TABLE 15: SEQUENCING ANALYSIS
# ============================================================================

both_users <- df %>%
  filter(both == 1) %>%
  mutate(
    btfp_first = btfp_first_date < dw_first_date,
    dw_first = dw_first_date < btfp_first_date,
    same_day = btfp_first_date == dw_first_date,
    gap_days = abs(as.numeric(btfp_first_date - dw_first_date)),
    sequence = case_when(btfp_first ~ "BTFP → DW", dw_first ~ "DW → BTFP",
                         same_day ~ "Same Day", TRUE ~ "Unknown")
  )

both_users %>%
  group_by(sequence) %>%
  summarise(
    N = n(),
    `%` = n() / nrow(both_users) * 100,
    `Avg Gap (days)` = mean(gap_days, na.rm = TRUE),
    `Avg MTM (BTFP)` = mean(mtm_btfp, na.rm = TRUE),
    `Avg Uninsured` = mean(uninsured_lev, na.rm = TRUE),
    `Avg BTFP Util` = mean(btfp_util, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(across(where(is.numeric), ~round(., 2))) %>%
  kable(caption = "**Table 15: Both-Facility Users - Sequencing**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 15: Both-Facility Users - Sequencing
sequence N % Avg Gap (days) Avg MTM (BTFP) Avg Uninsured Avg BTFP Util
BTFP → DW 120 28.24 82.36 0.89 27.63 3.06
DW → BTFP 261 61.41 100.31 0.78 28.38 4.05
Same Day 44 10.35 0.00 0.88 29.83 0.74

11 DW Insufficiency Analysis

11.1 Table 16: Was DW Sufficient?

# ============================================================================
# TABLE 16: DW INSUFFICIENCY - CAPACITY GAP ANALYSIS
# ============================================================================

# What BTFP provided that DW couldn't (due to haircuts)
df_btfp_gap <- df %>%
  filter(btfp == 1) %>%
  mutate(
    # Estimated DW capacity on same collateral (with ~15% haircut)
    dw_capacity = btfp_amount * 0.85,
    # Capacity gap = what BTFP provided above DW
    capacity_gap = btfp_amount - dw_capacity,
    capacity_gap_pct = capacity_gap / (total_asset * 1000) * 100
  )

# Summary
gap_summary <- df_btfp_gap %>%
  summarise(
    N = n(),
    `Mean Gap ($M)` = mean(capacity_gap / 1e6, na.rm = TRUE),
    `Median Gap ($M)` = median(capacity_gap / 1e6, na.rm = TRUE),
    `Total Gap ($B)` = sum(capacity_gap, na.rm = TRUE) / 1e9,
    `Mean Gap (% Assets)` = mean(capacity_gap_pct, na.rm = TRUE)
  )

cat("=== DW INSUFFICIENCY ANALYSIS ===\n")

=== DW INSUFFICIENCY ANALYSIS ===

cat("BTFP provided additional liquidity above what DW could have provided:\n")

BTFP provided additional liquidity above what DW could have provided:

print(gap_summary)

12 A tibble: 1 Ă— 5

  N `Mean Gap ($M)` `Median Gap ($M)` `Total Gap ($B)` `Mean Gap (% Assets)`

1 1305 46.7 4.17 61.0 1.95

# Regression: Does capacity gap correlate with MTM losses?
f_gap <- as.formula(paste("capacity_gap_pct ~ borrowing_subsidy + uninsured_lev +", CONTROLS))
m_gap <- feols(f_gap, data = df_btfp_gap, vcov = "hetero")

cat("\n\n")
modelsummary(
  list("Capacity Gap (% Assets)" = m_gap),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared"),
  title = "Table 16: DW Capacity Gap Analysis",
  notes = "Capacity Gap = What BTFP provided above DW (due to par valuation vs haircuts)"
)
Table 16: DW Capacity Gap Analysis
Capacity Gap (% Assets)
* p < 0.1, ** p < 0.05, *** p < 0.01
Capacity Gap = What BTFP provided above DW (due to par valuation vs haircuts)
Uninsured Leverage 0.018
(0.011)
Borrowing Subsidy -0.027
(0.019)
Log(Assets) 0.009
(0.088)
Cash Ratio -0.078***
(0.029)
Securities Ratio -0.012
(0.029)
Loan/Deposit -0.056**
(0.028)
Book Equity 0.114***
(0.044)
Wholesale Funding 0.101**
(0.047)
FHLB Ratio 0.161***
(0.044)
ROA 0.216
(0.258)
Num.Obs. 1259
R2 0.071

13 Robustness

13.1 Table 17: Alternative Estimators

# ============================================================================
# TABLE 17: LPM vs LOGIT vs PROBIT
# ============================================================================

f_main <- as.formula(paste("btfp ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +", CONTROLS))

m17_lpm <- feols(f_main, data = df, vcov = "hetero")
m17_logit <- glm(f_main, data = df, family = binomial(link = "logit"))
m17_probit <- glm(f_main, data = df, family = binomial(link = "probit"))

modelsummary(
  list("(1) LPM" = m17_lpm, "(2) Logit" = m17_logit, "(3) Probit" = m17_probit),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = COEF_MAP,
  gof_map = c("nobs", "r.squared", "AIC"),
  title = "Table 17: Alternative Estimators"
)
Table 17: Alternative Estimators
(1) LPM (2) Logit (3) Probit
* p < 0.1, ** p < 0.05, *** p < 0.01
MTM Loss (BTFP-Eligible) 0.065*** 0.441*** 0.256***
(0.019) (0.103) (0.062)
MTM Loss (Non-BTFP) 0.010*** 0.038** 0.022*
(0.003) (0.020) (0.012)
Uninsured Leverage 0.003*** 0.020*** 0.012***
(0.001) (0.004) (0.003)
MTM(BTFP) Ă— Uninsured -0.001 -0.011*** -0.007***
(0.001) (0.004) (0.002)
Log(Assets) 0.054*** 0.277*** 0.165***
(0.006) (0.031) (0.018)
Cash Ratio -0.005*** -0.020* -0.010*
(0.001) (0.011) (0.006)
Securities Ratio 0.002* 0.048*** 0.028***
(0.001) (0.010) (0.006)
Loan/Deposit -0.001** 0.030*** 0.017***
(0.001) (0.008) (0.005)
Book Equity -0.002*** -0.089*** -0.049***
(0.001) (0.013) (0.007)
Wholesale Funding 0.009*** 0.004 0.004
(0.003) (0.018) (0.011)
FHLB Ratio 0.007*** -0.008 -0.003
(0.002) (0.013) (0.008)
ROA -0.000 -0.055 -0.026
(0.007) (0.066) (0.038)
Num.Obs. 4678 4678 4678
R2 0.107

13.2 Model Performance

# Get predictions on complete cases only
df_complete <- df %>%
  select(btfp, mtm_btfp, mtm_other, uninsured_lev, ln_assets, cash_ratio, 
         securities_ratio, loan_to_deposit, book_equity_ratio, wholesale, fhlb_ratio, roa) %>%
  drop_na()

# Refit model on complete cases for prediction
m_auc <- glm(btfp ~ mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev) +
               ln_assets + cash_ratio + securities_ratio + loan_to_deposit + 
               book_equity_ratio + wholesale + fhlb_ratio + roa,
             data = df_complete, family = binomial(link = "logit"))

df_complete$pred <- predict(m_auc, type = "response")
roc_obj <- roc(df_complete$btfp, df_complete$pred, quiet = TRUE)

ggroc(roc_obj, color = COLORS$btfp, size = 1.2) +
  geom_abline(slope = 1, intercept = 1, linetype = "dashed", color = "gray50") +
  annotate("text", x = 0.3, y = 0.2, label = sprintf("AUC = %.3f", auc(roc_obj)), size = 5, fontface = "bold") +
  labs(title = "Figure 4: ROC Curve - BTFP Selection Model", 
       subtitle = sprintf("N = %d complete cases", nrow(df_complete)),
       x = "Specificity", y = "Sensitivity") +
  theme_pub()

14 Summary of Findings

findings <- tribble(
  ~Question, ~Finding, ~Evidence,

  "1. Par Valuation Selection",
  "Banks with higher MTM losses on BTFP-eligible securities systematically select BTFP",
  "Table 4: MTM(BTFP) positive and significant; MTM(Other) weaker",

  "2. Placebo Validation",
  "MTM(BTFP) matters MORE for BTFP than for pre-BTFP DW usage",
  "Table 6: Coefficient on MTM larger for BTFP than placebo",

  "3. Insolvency Channel",
  "MTM-insolvent banks more likely to access Fed facilities",
  "Table 7-8: Negative adjusted equity, positive MTM_Insolvent",

  "4. Temporal Evolution",
  "Acute: Run-driven; Post-Acute: Par valuation; Arbitrage: Rate",
  "Table 9-10: Coefficient magnitudes vary by period",

  "5. Intensive Margin",
  "Banks with larger subsidies borrow more conditional on participation",
  "Table 11: Borrowing Subsidy positive in amount regressions",

  "6. Multi-Facility Use",
  "Banks using both: Maxed out BTFP, needed non-OMO collateral",
  "Table 14: Maxed Out positive; Non-OMO ratio positive",

  "7. DW Insufficiency",
  "BTFP provided ~15% more capacity than DW due to par valuation",
  "Table 16: Capacity gap correlates with borrowing subsidy"
)

findings %>%
  kable(caption = "**Summary of Key Findings**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE) %>%
  column_spec(1, bold = TRUE, width = "22%") %>%
  column_spec(2, width = "40%") %>%
  column_spec(3, width = "38%")
Summary of Key Findings
Question Finding Evidence
  1. Par Valuation Selection
Banks with higher MTM losses on BTFP-eligible securities systematically select BTFP Table 4: MTM(BTFP) positive and significant; MTM(Other) weaker
  1. Placebo Validation
MTM(BTFP) matters MORE for BTFP than for pre-BTFP DW usage Table 6: Coefficient on MTM larger for BTFP than placebo
  1. Insolvency Channel
MTM-insolvent banks more likely to access Fed facilities Table 7-8: Negative adjusted equity, positive MTM_Insolvent
  1. Temporal Evolution
Acute: Run-driven; Post-Acute: Par valuation; Arbitrage: Rate Table 9-10: Coefficient magnitudes vary by period
  1. Intensive Margin
Banks with larger subsidies borrow more conditional on participation Table 11: Borrowing Subsidy positive in amount regressions
  1. Multi-Facility Use
Banks using both: Maxed out BTFP, needed non-OMO collateral Table 14: Maxed Out positive; Non-OMO ratio positive
  1. DW Insufficiency
BTFP provided ~15% more capacity than DW due to par valuation Table 16: Capacity gap correlates with borrowing subsidy

15 Conclusion

This analysis provides evidence for the causal mechanisms driving bank facility choice during the 2023 banking crisis:

  1. Par Valuation Creates Selection: Banks with larger MTM losses on BTFP-eligible securities systematically selected BTFP to capture the par valuation subsidy, while MTM losses on non-eligible securities did not predict BTFP usage.

  2. Placebo Validates Identification: The effect is weaker for pre-BTFP DW usage when par valuation didn’t exist, supporting our identification strategy.

  3. Vulnerable Banks Accessed Facilities: MTM-insolvent banks (negative adjusted equity) were more likely to access Fed facilities, with BTFP providing particular value.

  4. Borrowing Determinants Evolved: From run-driven (acute) to subsidy-driven (post-acute) to rate-driven (arbitrage).

  5. BTFP Filled a Gap: BTFP provided approximately 15% more liquidity than DW could have on the same collateral, addressing DW insufficiency.


Analysis completed: December 26, 2025 at 08:00

sessionInfo()

R version 4.3.1 (2023-06-16 ucrt) Platform: x86_64-w64-mingw32/x64 (64-bit) Running under: Windows 11 x64 (build 26200)

Matrix products: default

locale: [1] LC_COLLATE=English_United States.utf8 LC_CTYPE=English_United States.utf8 LC_MONETARY=English_United States.utf8 [4] LC_NUMERIC=C LC_TIME=English_United States.utf8

time zone: America/Chicago tzcode source: internal

attached base packages: [1] stats graphics grDevices utils datasets methods base

other attached packages: [1] psych_2.5.6 viridis_0.6.5 viridisLite_0.4.2 scales_1.4.0 patchwork_1.3.2
[6] kableExtra_1.4.0 modelsummary_2.4.0 sampleSelection_1.2-12 maxLik_1.5-2.1 miscTools_0.6-28
[11] pROC_1.18.5 margins_0.3.28 broom_1.0.9 lmtest_0.9-40 zoo_1.8-13
[16] sandwich_3.1-1 fixest_0.12.1 data.table_1.17.0 lubridate_1.9.4 forcats_1.0.0
[21] stringr_1.5.1 dplyr_1.1.4 purrr_1.0.4 readr_2.1.5 tidyr_1.3.1
[26] tibble_3.2.1 ggplot2_3.5.2 tidyverse_2.0.0

loaded via a namespace (and not attached): [1] mnormt_2.1.1 gridExtra_2.3 rlang_1.1.1 magrittr_2.0.3 dreamerr_1.4.0
[6] prediction_0.3.18 compiler_4.3.1 systemfonts_1.2.2 vctrs_0.6.5 pkgconfig_2.0.3
[11] crayon_1.5.3 fastmap_1.2.0 backports_1.5.0 labeling_0.4.3 rmarkdown_2.29
[16] tzdb_0.5.0 bit_4.6.0 xfun_0.52 cachem_1.1.0 litedown_0.7
[21] jsonlite_2.0.0 tinytable_0.13.0 stringmagic_1.1.2 systemfit_1.1-30 VGAM_1.1-13
[26] parallel_4.3.1 R6_2.6.1 bslib_0.9.0 tables_0.9.31 stringi_1.8.7
[31] RColorBrewer_1.1-3 car_3.1-3 jquerylib_0.1.4 numDeriv_2016.8-1.1 estimability_1.5.1 [36] Rcpp_1.0.14 knitr_1.50 parameters_0.27.0 Matrix_1.5-4.1 splines_4.3.1
[41] timechange_0.3.0 tidyselect_1.2.1 rstudioapi_0.17.1 abind_1.4-8 yaml_2.3.10
[46] lattice_0.21-8 plyr_1.8.9 withr_3.0.2 bayestestR_0.16.1 coda_0.19-4.1
[51] evaluate_1.0.4 xml2_1.3.8 pillar_1.11.0 carData_3.0-5 checkmate_2.3.2
[56] stats4_4.3.1 insight_1.3.1 generics_0.1.4 vroom_1.6.5 hms_1.1.3
[61] xtable_1.8-4 glue_1.8.0 emmeans_1.11.2-8 tools_4.3.1 mvtnorm_1.3-3
[66] grid_4.3.1 datawizard_1.2.0 nlme_3.1-162 performance_0.15.0 Formula_1.2-5
[71] cli_3.6.1 fansi_1.0.6 svglite_2.1.3 gtable_0.3.6 sass_0.4.10
[76] digest_0.6.33 farver_2.1.2 htmltools_0.5.9 lifecycle_1.0.4 bit64_4.6.0-1
[81] MASS_7.3-60