Summary

This document provides a comprehensive analysis of bank borrowing behavior across Federal Reserve emergency facilities (BTFP and Discount Window) during the March 2023 banking crisis.

Research Questions

  1. Extensive Margin: Did BTFP’s par valuation generate systematic selection, with banks sorting into facilities based on MTM losses?
  2. Temporal Dynamics: Did borrowing determinants differ across crisis phases (acute crisis vs. post-acute vs. arbitrage)?
  3. Intensive Margin: Conditional on borrowing, did banks with larger MTM losses borrow more?
  4. Collateral Constraints: Did banks “max out” BTFP-eligible collateral and turn to DW for additional needs?

Empirical Strategy Overview

Step Analysis Question Answered
1 Extensive margin (full period) Did MTM losses drive BTFP selection?
2 Temporal analysis Did determinants differ across phases?
3 Intensive margin Did distressed banks borrow more?
4 “Both” banks Did collateral constraints bind?

Variable Definitions

Dependent Variables

Variable Definition Formula
BTFP_i Binary: Bank i borrowed from BTFP 1[Bank i borrowed from BTFP]
DW_i Binary: Bank i borrowed from DW 1[Bank i borrowed from DW]
BTFPAmount_i BTFP borrowing scaled by assets BTFP Borrowing_i / Assets_i
BTFP_Acute_i First BTFP borrow <= May 1, 2023 1[First BTFP date <= May 1]
BTFP_PostAcute_i First BTFP borrow in (May 1, Oct 31] 1[First BTFP date in (May 1, Oct 31]]
BTFP_Arb_i First BTFP borrow in (Nov 1, Jan 24] 1[First BTFP date in (Nov 1, Jan 24]]

Key Explanatory Variables

Variable Definition Formula
MTM_BTFP_i MTM loss on BTFP-eligible securities / Assets MTM Loss on OMO-Eligible_i / Total Assets_i
MTM_Other_i MTM loss on non-eligible assets / Assets MTM Loss on Non-OMO_i / Total Assets_i
UninsuredLev_i Uninsured deposits / Assets Uninsured Deposits_i / Total Assets_i
EligibleCollateral_i BTFP-eligible securities / Assets BTFP-Eligible Securitiess_i / Total Assets_i
BorrowingSubsidy_i MTM loss rate on eligible collateral MTM Loss on OMO-Eligible_i / OMO-Eligible_i

Run Risk Measures

Variable Definition Formula
RunRisk1_i Continuous: Uninsured x MTM %Uninsured_i x %MTM Loss_i
RunRisk2_i Continuous: Runable x MTM %Runable_i x %MTM Loss_i
RunRisk1Dummy_i Binary: Both above median 1[%Uninsured > p50 AND %MTM > p50]
RunRisk2Dummy_i Binary: Both above median 1[%Runable > p50 AND %MTM > p50]

Jiang et al. Insolvency Measures

Insured Deposit Coverage Ratio (IDCR)

IDCR_i(s) = (MV_Assets_i - s x UninsuredDeposits_i - InsuredDeposits_i) / InsuredDeposits_i

where s in {0.5, 1.0} represents the fraction of uninsured deposits that run.

Variable Run Scenario Interpretation
IDCR_1 s = 0.5 (50% run) Coverage ratio under moderate run
IDCR_2 s = 1.0 (100% run) Coverage ratio under complete run

Insolvency: Bank is insolvent if IDCR < 0

Capital Ratio Metric

Insolvency_i(s) = [(TotalAssets_i - TotalLiabilities_i) - s x UninsuredDeposits_i x MV_Adjustment_i] / TotalAssets_i

where: MV_Adjustment_i = (TotalAssets_i / MV_Assets_i) - 1

Variable Run Scenario Interpretation
Insolvency_1 s = 0.5 Capital metric under 50% run
Insolvency_2 s = 1.0 Capital metric under 100% run

Adjusted Equity (Jiang-Style)

AdjustedEquity_i = EquityRatio_i - MTMLoss_i

MTM_Insolvent_i = 1[AdjustedEquity_i < 0]

Control Variables

Variable Definition Formula
ln_assets Log of total assets ln(Total Assets_i)
cash_ratio Cash / Assets Cash_i / Total Assets_i
securities_ratio Securities / Assets Securities_i / Total Assets_i
roa Return on assets Net Income_i / Avg Assets_i
loan_to_deposit Loans / Deposits Total Loans_i / Total Deposits_i
book_equity_ratio Book equity / Assets Total Equity_i / Total Assets_i
pct_wholesale_liability Wholesale funding / Liabilities (Fed Funds + Repo + Other Borr)_i / Total Liabilities_i
pct_runable_liability Runable liabilities / Liabilities (Uninsured + Wholesale)_i / Total Liabilities_i
pct_liquidity_available Liquid assets / Assets (Cash + Rerepo + FF Sold)_i / Total Assets_i
fhlb_ratio FHLB advances / Assets FHLB Advances_i / Total Assets_i

Facility Choice Variables

Variable Definition
btfp Binary: Used BTFP during full program
dw Binary: Used DW during full program (post-BTFP)
dw_pre_btfp Binary: Used DW in pre-BTFP period (Jan 1 - Mar 12, 2023)
both Binary: Used both BTFP and DW
btfp_only Binary: Used BTFP but not DW
dw_only Binary: Used DW but not BTFP
any_fed Binary: Used either facility
facility Categorical: None / BTFP Only / DW Only / Both

Facility Design Variables (Loan-Level Aggregated to Bank)

Variable Definition Source
btfp_avg_term Weighted avg BTFP loan term (days) Loan data
btfp_avg_rate Weighted avg BTFP interest rate (%) Loan data
btfp_n_loans Number of BTFP loans Loan data
btfp_max_term Maximum BTFP term chosen Loan data
dw_avg_term Weighted avg DW loan term (days) Loan data
dw_avg_rate Weighted avg DW interest rate (%) Loan data
dw_n_loans Number of DW loans Loan data
dw_max_term Maximum DW term chosen Loan data
rate_diff DW rate - BTFP rate Loan data

Facility Design Choice Indicators

Variable Definition Interpretation
high_uninsured Above-median uninsured deposits Needs stable funding
high_wholesale Above-median wholesale funding Needs stable funding
funding_stability_need high_uninsured + high_wholesale (0-2) Higher = more need for BTFP 1-yr term
collateral_constrained Below-median OMO-eligible collateral May need DW
low_liquidity Below-median liquid assets Acute liquidity need
stress_indicator high_uninsured × high_mtm_loss Run risk measure

Intensive Margin Variables

Variable Definition Formula
btfp_amount_pct BTFP borrowing as % of assets (BTFP Amount / Assets) x 100
dw_amount_pct DW borrowing as % of assets (DW Amount / Assets) x 100
total_borrowed_pct Total Fed borrowing as % of assets (BTFP + DW Amount) / Assets x 100
btfp_share BTFP share of total Fed borrowing BTFP Amount / (BTFP + DW) x 100
btfp_utilization Collateral utilization rate BTFP Amount / Collateral Capacity
maxed_out_btfp Binary: Utilization > 90% 1[btfp_utilization > 0.90]

Step 1: Extensive Margin (Full Period)

\[BTFP_i = \alpha + \beta_1 \cdot MTM^{BTFP}_i + \beta_2 \cdot MTM^{Other}_i + \beta_3 \cdot UninsuredLev_i + \beta_4 \cdot (MTM^{BTFP}_i \times UninsuredLev_i) + \beta_5 \cdot EligibleCollateral_i + \gamma'Z_i + \varepsilon_i\]

Run separately for BTFP and DW as dependent variables.

Step 2: Temporal Analysis

For each period \(p \in \{Acute, PostAcute, Arbitrage\}\):

\[BTFP^p_i = \alpha + \beta^p_1 \cdot MTM^{BTFP}_i + \beta^p_2 \cdot MTM^{Other}_i + \beta^p_3 \cdot UninsuredLev_i + \gamma'Z_i + \varepsilon_i\]

Step 3: Intensive Margin

Among BTFP users:

\[\frac{BTFPAmount_i}{Assets_i} = \alpha + \beta_1 \cdot MTM^{BTFP}_i + \beta_2 \cdot MTM^{Other}_i + \beta_3 \cdot UninsuredLev_i + \gamma'Z_i + \varepsilon_i\]

Step 4: Both Banks and Collateral Constraints

\[DW_i = \alpha + \beta_1 \cdot MTM^{BTFP}_i + \beta_2 \cdot MTM^{Other}_i + \beta_3 \cdot EligibleCollateral_i + \gamma'Z_i + \varepsilon_i \quad | \quad BTFP_i = 1\]

Part VI

Facility Design Choice Models

These models test whether facility design features (term, rate, maturity) drive bank selection:

Model 1: Funding Stability Need

Banks with unstable funding sources (high uninsured/wholesale) should prefer BTFP’s 1-year term.

Model 2: Collateral Constraints

Banks with limited OMO-eligible collateral may need DW even if BTFP is preferable.

Model 3: Loan Term Determinants (Among Borrowers)

Do stressed banks choose longer loan terms within each facility?

Model 4: Rate Differential Analysis For banks using both facilities: Is the BTFP-DW rate differential reflected in borrowing allocation?

Document Structure:

  • Part I: Setup, Data Loading, and Period Definitions
  • Part II: Descriptive Statistics and Summary Tables
  • Part III: Regression Analysis (Extensive, Temporal, Intensive Margin)
  • Part IV: Robustness Tests
  • Part V: Visualizations
  • Part VI : Facility Design Choice Models
  • Appendix: Variable Dictionary and Session Info

PART I: SETUP, DATA LOADING, AND PERIOD DEFINITIONS

1.1 Package Installation and Loading

# ==========================================================================
# SETUP AND PACKAGES
# ==========================================================================

BASE_PATH <- "C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025"
knitr::opts_knit$set(root.dir = BASE_PATH)

options(repos = c(CRAN = "https://cloud.r-project.org"))
options(scipen = 999)
set.seed(20230313)

# Required packages
required_packages <- c(
  "data.table", "dplyr", "tidyr", "tibble", "lubridate", "stringr", "readr",
  "fixest", "sandwich", "lmtest", "broom",
  "ggplot2", "ggthemes", "scales", "patchwork", "gridExtra",
  "knitr", "kableExtra", "modelsummary", 
  "DescTools", "moments", "psych", "tidyverse"
)

# Install missing packages
missing <- required_packages[!required_packages %in% installed.packages()[,"Package"]]
if(length(missing) > 0) install.packages(missing)

# Load packages
suppressPackageStartupMessages({
  library(data.table)
  library(dplyr)
  library(tidyr)
  library(tibble)
  library(lubridate)
  library(stringr)
  library(readr)
  library(fixest)
  library(sandwich)
  library(lmtest)
  library(broom)
  library(ggplot2)
  library(ggthemes)
  library(scales)
  library(patchwork)
  library(gridExtra)
  library(knitr)
  library(kableExtra)
  library(modelsummary)
  library(DescTools)
  library(moments)
  library(psych)
  library(tidyverse)
})

cat("✓ All packages loaded successfully\n")
#> ✓ All packages loaded successfully

1.2 Configure Paths

# ==========================================================================
# CONFIGURE PATHS
# ==========================================================================

DATA_RAW    <- file.path(BASE_PATH, "01_data/raw")
DATA_PROC   <- file.path(BASE_PATH, "01_data/processed")
DATA_OUT    <- file.path(BASE_PATH, "01_data/output")
DOC_PATH    <- file.path(BASE_PATH, "03_documentation")
TABLE_PATH  <- file.path(DOC_PATH, "regression_tables/btfp_analysis_v06")
FIG_PATH    <- file.path(DOC_PATH, "figures/btfp_analysis_v06")

# Create output directories
dir.create(TABLE_PATH, recursive = TRUE, showWarnings = FALSE)
dir.create(FIG_PATH, recursive = TRUE, showWarnings = FALSE)

cat("✓ Paths configured\n")
#> ✓ Paths configured
cat("  Data (raw):", DATA_RAW, "\n")
#>   Data (raw): C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/01_data/raw
cat("  Data (processed):", DATA_PROC, "\n")
#>   Data (processed): C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/01_data/processed
cat("  Tables:", TABLE_PATH, "\n")
#>   Tables: C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/regression_tables/btfp_analysis_v06
cat("  Figures:", FIG_PATH, "\n")
#>   Figures: C:/Users/mohua/OneDrive - Louisiana State University/Finance_PhD/DW_Stigma_paper/Liquidity_project_2025/03_documentation/figures/btfp_analysis_v06

1.3 Period Definitions

# ==========================================================================
# PERIOD DEFINITIONS
# ==========================================================================
#
# TWO PERIOD CLASSIFICATION SYSTEMS:
# 
# SYSTEM 1 (OLD): Three broad periods
#   - Acute: Mar 13 - May 1, 2023
#   - Post-Acute: May 2 - Oct 31, 2023
#   - Arbitrage: Nov 1, 2023 - Jan 24, 2024
#
# SYSTEM 2 (NEW): Seven granular periods
#   - P0 (Pre-BTFP): Oct 1, 2022 - Mar 12, 2023
#   - P1 (Week 1): Mar 13-19, 2023
#   - P2 (Crisis Month): Mar 20 - Apr 30, 2023
#   - P3 (FRC May): May 1-31, 2023
#   - P4 (Stabilization): Jun 1 - Sep 30, 2023
#   - P5 (Arbitrage): Nov 1, 2023 - Jan 24, 2024
#   - P6 (Wind-down): Jan 25 - Mar 11, 2024
#
# NOTE: DW data ends September 30, 2023
# ==========================================================================

## Key Crisis Dates
PRE_BTFP_START      <- as.Date("2022-10-01")
SVB_FAIL            <- as.Date("2023-03-10")  # Friday
SIGNATURE_FAIL      <- as.Date("2023-03-12")  # Sunday
PRE_BTFP_END        <- as.Date("2023-03-12")

CRISIS_START        <- as.Date("2023-03-13")  # Monday - BTFP operational
WEEK1_END           <- as.Date("2023-03-19")  # End of first week
WEEK2_END           <- as.Date("2023-03-26")  # End of second week
MARCH_END           <- as.Date("2023-03-31")
APRIL_END           <- as.Date("2023-04-30")

FRC_FAIL            <- as.Date("2023-05-01")  # First Republic fails
MAY_END             <- as.Date("2023-05-31")

SUMMER_START        <- as.Date("2023-06-01")
DW_DATA_END         <- as.Date("2023-09-30")  # CRITICAL: Last DW data

DW_HAIRCUT_RETURN   <- as.Date("2023-10-31")  # DW haircuts return

ARBITRAGE_START     <- as.Date("2023-11-01")
ARB_WINDOW_OPEN     <- as.Date("2023-11-06")  # BTFP rate < IORB
ARBITRAGE_END       <- as.Date("2024-01-24")  # Fed closes new BTFP

BTFP_CLOSE          <- as.Date("2024-03-11")  # BTFP fully closes

## Old Period Definitions (Legacy)
PERIOD_1_END        <- as.Date("2023-05-01")   # Acute ends
PERIOD_2_END        <- as.Date("2023-10-31")   # Post-Acute ends
PERIOD_3_END        <- as.Date("2024-01-24")   # Arbitrage ends

## New Period Definitions
WEEK_1_END          <- WEEK1_END
CRISIS_MONTH_END    <- APRIL_END

BASELINE_QUARTER    <- "2022Q4"
BASELINE_DATE       <- "2022Q4"

cat("✓ Period definitions established\n")
#> ✓ Period definitions established
cat("\n=== OLD PERIOD SYSTEM ===\n")
#> 
#> === OLD PERIOD SYSTEM ===
cat("Acute:      ", format(CRISIS_START), "to", format(PERIOD_1_END), "\n")
#> Acute:       2023-03-13 to 2023-05-01
cat("Post-Acute: ", format(PERIOD_1_END + 1), "to", format(PERIOD_2_END), "\n")
#> Post-Acute:  2023-05-02 to 2023-10-31
cat("Arbitrage:  ", format(PERIOD_2_END + 1), "to", format(PERIOD_3_END), "\n")
#> Arbitrage:   2023-11-01 to 2024-01-24
cat("\n=== NEW PERIOD SYSTEM ===\n")
#> 
#> === NEW PERIOD SYSTEM ===
cat("P0 (Pre-BTFP):       ", format(PRE_BTFP_START), "to", format(PRE_BTFP_END), "\n")
#> P0 (Pre-BTFP):        2022-10-01 to 2023-03-12
cat("P1 (Week 1):         ", format(CRISIS_START), "to", format(WEEK1_END), "\n")
#> P1 (Week 1):          2023-03-13 to 2023-03-19
cat("P2 (Crisis Month):   ", format(WEEK1_END + 1), "to", format(APRIL_END), "\n")
#> P2 (Crisis Month):    2023-03-20 to 2023-04-30
cat("P3 (FRC May):        ", format(FRC_FAIL), "to", format(MAY_END), "\n")
#> P3 (FRC May):         2023-05-01 to 2023-05-31
cat("P4 (Stabilization):  ", format(SUMMER_START), "to", format(DW_DATA_END), "\n")
#> P4 (Stabilization):   2023-06-01 to 2023-09-30
cat("P5 (Arbitrage):      ", format(ARBITRAGE_START), "to", format(ARBITRAGE_END), "\n")
#> P5 (Arbitrage):       2023-11-01 to 2024-01-24
cat("P6 (Wind-down):      ", format(ARBITRAGE_END + 1), "to", format(BTFP_CLOSE), "\n")
#> P6 (Wind-down):       2024-01-25 to 2024-03-11
cat("\n*** NOTE: DW data ends", format(DW_DATA_END), "***\n")
#> 
#> *** NOTE: DW data ends 2023-09-30 ***

1.4 Helper Functions

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

## Data Manipulation Functions
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_div <- function(num, denom) {
  ifelse(is.na(denom) | denom == 0, NA_real_, num / denom)
}

parse_date_safe <- function(x) {
  if (inherits(x, "Date")) return(x)
  x_char <- as.character(x)
  result <- suppressWarnings(mdy(x_char))
  if (all(is.na(result))) result <- suppressWarnings(ymd(x_char))
  if (all(is.na(result))) result <- suppressWarnings(dmy(x_char))
  return(result)
}

## Period Assignment Functions

# Assign OLD period (Acute, Post-Acute, Arbitrage)
assign_period_old <- function(date) {
  case_when(
    date >= CRISIS_START & date <= PERIOD_1_END ~ "Acute",
    date > PERIOD_1_END & date <= PERIOD_2_END ~ "Post-Acute",
    date > PERIOD_2_END & date <= PERIOD_3_END ~ "Arbitrage",
    date < CRISIS_START ~ "Pre-BTFP",
    TRUE ~ NA_character_
  )
}

# Assign NEW period (P0-P6)
assign_period_new <- function(date) {
  case_when(
    date >= PRE_BTFP_START & date <= PRE_BTFP_END ~ "P0_PreBTFP",
    date >= CRISIS_START & date <= WEEK1_END ~ "P1_Week1",
    date > WEEK1_END & date <= CRISIS_MONTH_END ~ "P2_CrisisMonth",
    date > CRISIS_MONTH_END & date <= MAY_END ~ "P3_FRCMay",
    date > MAY_END & date <= DW_DATA_END ~ "P4_Stabilization",
    date >= ARBITRAGE_START & date <= ARBITRAGE_END ~ "P5_Arbitrage",
    date > ARBITRAGE_END & date <= BTFP_CLOSE ~ "P6_WindDown",
    TRUE ~ NA_character_
  )
}

# Get call report quarter for a given date
get_call_quarter <- function(loan_date) {
  case_when(
    loan_date <= as.Date("2023-03-31") ~ "2022Q4",
    loan_date <= as.Date("2023-06-30") ~ "2023Q1",
    loan_date <= as.Date("2023-09-30") ~ "2023Q2",
    loan_date <= as.Date("2023-12-31") ~ "2023Q3",
    TRUE ~ "2023Q4"
  )
}

## Significance Functions
add_stars <- function(p_val) {
  if (is.na(p_val)) return("")
  if (p_val < 0.01) return("***")
  if (p_val < 0.05) return("**")
  if (p_val < 0.10) return("*")
  return("")
}

cat("✓ Helper functions defined\n")
#> ✓ Helper functions defined

1.5 Theme and Color Schemes

# ==========================================================================
# THEME AND COLOR SCHEMES
# ==========================================================================

## Publication-quality theme
theme_pub <- function(base_size = 11) {
  theme_minimal(base_size = base_size) +
    theme(
      plot.title = element_text(face = "bold", size = rel(1.2), hjust = 0),
      plot.subtitle = element_text(size = rel(0.9), hjust = 0, color = "gray40"),
      plot.caption = element_text(size = rel(0.7), hjust = 1, color = "gray50"),
      panel.grid.minor = element_blank(),
      panel.grid.major = element_line(color = "gray90", linewidth = 0.3),
      axis.title = element_text(face = "bold", size = rel(0.9)),
      legend.position = "bottom",
      strip.text = element_text(face = "bold"),
      plot.margin = margin(10, 10, 10, 10)
    )
}

## Color schemes
crisis_colors <- c(
  "P0_PreBTFP" = "#636363",
  "P1_Week1" = "#d73027",
  "P2_CrisisMonth" = "#fc8d59",
  "P3_FRCMay" = "#fee08b",
  "P4_Stabilization" = "#d9ef8b",
  "P5_Arbitrage" = "#91bfdb",
  "P6_WindDown" = "#4575b4",
  "Acute" = "#d73027",
  "Post-Acute" = "#fee08b",
  "Arbitrage" = "#91bfdb"
)

facility_colors <- c(
  "BTFP" = "#2166ac",
  "DW" = "#b2182b",
  "Both" = "#762a83",
  "None" = "#d9d9d9"
)

cat("✓ Themes and colors configured\n")
#> ✓ Themes and colors configured

1.6 Load Data

# ==========================================================================
# LOAD DATA
# ==========================================================================

cat("\n=== LOADING DATA ===\n")
#> 
#> === LOADING DATA ===
## Load Call Reports
call_q <- read_csv(file.path(DATA_PROC, "final_call_gsib.csv"), show_col_types = FALSE) %>%
  mutate(idrssd = as.character(idrssd))
cat("✓ Call reports loaded:", nrow(call_q), "observations\n")
#> ✓ Call reports loaded: 61002 observations
## Load BTFP Loans
btfp_raw <- read_csv(file.path(DATA_PROC, "btfp_loan_bank_only.csv"), show_col_types = FALSE) %>%
  mutate(
    rssd_id = as.character(rssd_id),
    btfp_loan_date = parse_date_safe(btfp_loan_date),
    btfp_maturity_date = parse_date_safe(btfp_maturity_date),
    btfp_repayment_date = parse_date_safe(btfp_repayment_date)
  )
cat("✓ BTFP loans loaded:", nrow(btfp_raw), "records\n")
#> ✓ BTFP loans loaded: 6734 records
## Load DW Loans
dw_raw <- read_csv(file.path(DATA_PROC, "dw_loan_bank_2023.csv"), show_col_types = FALSE) %>%
  mutate(
    rssd_id = as.character(rssd_id),
    dw_loan_date = parse_date_safe(dw_loan_date),
    dw_maturity_date = parse_date_safe(dw_maturity_date),
    dw_repayment_date = parse_date_safe(dw_repayment_date)
  )
cat("✓ DW loans loaded:", nrow(dw_raw), "records\n")
#> ✓ DW loans loaded: 20219 records
cat("\n✓ All data loaded successfully\n")
#> 
#> ✓ All data loaded successfully

1.7 Create Loan-Level Datasets with Period Assignments

# ==========================================================================
# CREATE LOAN-LEVEL DATASETS WITH PERIOD ASSIGNMENTS
# ==========================================================================

cat("\n=== CREATING LOAN-LEVEL DATASETS ===\n")
#> 
#> === CREATING LOAN-LEVEL DATASETS ===
## BTFP Loans with both period systems
btfp_loans <- btfp_raw %>%
  mutate(
    period_old = assign_period_old(btfp_loan_date),
    period_new = assign_period_new(btfp_loan_date),
    facility = "BTFP"
  ) %>%
  filter(!is.na(period_old) | !is.na(period_new))

cat("✓ BTFP loans with periods:", nrow(btfp_loans), "\n")
#> ✓ BTFP loans with periods: 6734
cat("  Old periods:", table(btfp_loans$period_old), "\n")
#>   Old periods: 1087 3279 1972
cat("  New periods:", table(btfp_loans$period_new), "\n")
#>   New periods: 104 958 762 1033 3279 396
## DW Loans with both period systems  
dw_loans <- dw_raw %>%
  mutate(
    period_old = assign_period_old(dw_loan_date),
    period_new = assign_period_new(dw_loan_date),
    facility = "DW"
  ) %>%
  filter(!is.na(period_old) | !is.na(period_new))

cat("✓ DW loans with periods:", nrow(dw_loans), "\n")
#> ✓ DW loans with periods: 20219
cat("  Old periods:", table(dw_loans$period_old), "\n")
#>   Old periods: 1619 4237 14363
cat("  New periods:", table(dw_loans$period_new), "\n")
#>   New periods: 4189 336 1241 878 3401

1.8 Create Dual Baselines

# ==========================================================================
# CREATE DUAL BASELINES
# ==========================================================================
#
# BASELINE 1 (Restricted): 
#   - Exclude failed banks (failed_bank == 1)
#   - Exclude GSIBs (gsib == 1)
#   - Require OMO-eligible assets > 0
#   - Focus on "regular" banks that could meaningfully choose BTFP
#
# BASELINE 2 (Full):
#   - Include all banks with valid data
#   - No filtering except valid total_asset > 0
#   - Broader sample for robustness
# ==========================================================================

cat("\n=== CREATING DUAL BASELINES ===\n")
#> 
#> === CREATING DUAL BASELINES ===
## Common transformation function
prepare_baseline_vars <- function(data) {
  
  # --- FIX: Ensure columns exist before the pipe ---
  # We check the schema first. If columns are missing, we create them 
  # with the correct default values (0 for flags, NA for categories).
  
  if (!"failed_bank" %in% names(data)) data$failed_bank <- 0
  if (!"gsib" %in% names(data)) data$gsib <- 0
  if (!"size_bin" %in% names(data)) data$size_bin <- NA_character_
  
  data %>%
    transmute(
      idrssd = as.character(idrssd),
      failed_bank,  # Safe to select now because we ensured it exists above
      gsib,         
      size_bin,     
      
      # Assets and liabilities
      total_asset, total_liability, total_equity, total_deposit,
      uninsured_deposit, 
      insured_deposit = pmax(insured_deposit, 1),
      uninsured_deposit_to_total_asset,
      
      # Size
      ln_assets = log(pmax(total_asset, 1)),
      assets = total_asset,
      
      # Liquid assets
      cash, fed_fund_sold, rerepo,
      
      # Securities
      security, omo_eligible, non_omo_eligible,
      omo_eligible_to_total_asset, non_omo_eligible_to_total_asset,
      
      # Loans and profitability
      total_loan, roa,
      
      # Market value and MTM
      mv_asset = mm_asset,
      mtm_total_loss, mtm_loss_to_total_asset, equity_after_mtm,
      mtm_loss_omo_eligible, mtm_loss_omo_eligible_to_total_asset,
      mtm_loss_omo_eligible_to_omo_eligible,
      mtm_loss_non_omo_eligible, mtm_loss_non_omo_eligible_to_total_asset,
      
      # Funding
      repo, fed_fund_purchase, other_borr, other_borrowed_less_than_1yr,
      fhlb_adv, fhlb_to_total_asset,
      
      # Ratios
      cash_to_total_asset, security_to_total_asset,
      book_equity_to_total_asset, loan_to_deposit
    )
}

cat("\n=== CREATING DUAL BASELINES (FIXED) ===\n")
#> 
#> === CREATING DUAL BASELINES (FIXED) ===
## BASELINE 1: Restricted
baseline_1 <- call_q %>%
  filter(quarter == BASELINE_DATE) %>%
  prepare_baseline_vars() %>%
  filter(
    (gsib == 0 | is.na(gsib)),
    (failed_bank == 0 | is.na(failed_bank)),
    !is.na(omo_eligible) & omo_eligible > 0
  )

cat("✓ BASELINE 1 (Restricted) created\n")
#> ✓ BASELINE 1 (Restricted) created
cat("  Sample size:", nrow(baseline_1), "banks\n")
#>   Sample size: 4292 banks
## BASELINE 2: Full
baseline_2 <- call_q %>%
  filter(quarter == BASELINE_DATE) %>%
  prepare_baseline_vars() %>%
  filter(!is.na(total_asset) & total_asset > 0)

cat("✓ BASELINE 2 (Full) created\n")
#> ✓ BASELINE 2 (Full) created
cat("  Sample size:", nrow(baseline_2), "banks\n")
#>   Sample size: 4737 banks
cat("  Includes:\n")
#>   Includes:
cat("    - GSIBs:", sum(baseline_2$gsib == 1, na.rm = TRUE), "\n")
#>     - GSIBs: 33
cat("    - Failed banks:", sum(baseline_2$failed_bank == 1, na.rm = TRUE), "\n")
#>     - Failed banks: 8
cat("\n✓ Dual baselines created successfully\n")
#> 
#> ✓ Dual baselines created successfully

1.9 Construct Analysis Variables

# ==========================================================================
# CONSTRUCT ANALYSIS VARIABLES - Apply to both baselines
# ==========================================================================

cat("\n=== CONSTRUCTING ANALYSIS VARIABLES ===\n")
#> 
#> === CONSTRUCTING ANALYSIS VARIABLES ===
construct_analysis_vars <- function(data) {
  data %>%
    mutate(
      # Core selection variables
    mtm_btfp = mtm_loss_omo_eligible_to_total_asset,
    mtm_other = mtm_loss_non_omo_eligible_to_total_asset,
    uninsured_lev = uninsured_deposit_to_total_asset,
    eligible_collateral = omo_eligible_to_total_asset,
    borrowing_subsidy = mtm_loss_omo_eligible_to_omo_eligible,
    pct_uninsured = safe_div(uninsured_deposit, total_deposit) * 100,
    pct_wholesale_liability = safe_div(fed_fund_purchase + repo + other_borrowed_less_than_1yr, total_liability) * 100,
    pct_runable_liability = safe_div(uninsured_deposit + fed_fund_purchase + repo + other_borrowed_less_than_1yr, total_liability) * 100,
    pct_liquidity_available = safe_div(cash + rerepo + fed_fund_sold, total_asset) * 100,
    pct_mtm_loss = mtm_loss_to_total_asset,
    cash_ratio = cash_to_total_asset,
    securities_ratio = security_to_total_asset,
    equity_ratio = safe_div(total_equity, total_asset) * 100,
    book_equity_ratio = book_equity_to_total_asset,
    fhlb_ratio = fhlb_to_total_asset,
    loan_to_deposit = loan_to_deposit,
    run_risk_1 = pct_uninsured * pct_mtm_loss,
    run_risk_2 = pct_runable_liability * pct_mtm_loss
    ) %>%
    
    
    # Calculate medians for dummy variables
    mutate(
    median_pct_uninsured = median(pct_uninsured, na.rm = TRUE),
    median_pct_mtm_loss = median(pct_mtm_loss, na.rm = TRUE),
    median_pct_runable = median(pct_runable_liability, na.rm = TRUE),
    run_risk_1_dummy = as.integer(pct_uninsured > median_pct_uninsured & pct_mtm_loss > median_pct_mtm_loss),
    run_risk_2_dummy = as.integer(pct_runable_liability > median_pct_runable & pct_mtm_loss > median_pct_mtm_loss)
    ) %>%
    # Jiang insolvency measures
    mutate(
    mv_adjustment = if_else(mv_asset == 0 | is.na(mv_asset), NA_real_, (total_asset / mv_asset) - 1),
    idcr_1 = safe_div(mv_asset - 0.5 * uninsured_deposit - insured_deposit, insured_deposit),
    idcr_2 = safe_div(mv_asset - 1.0 * uninsured_deposit - insured_deposit, insured_deposit),
    insolvency_1 = safe_div((total_asset - total_liability) - 0.5 * uninsured_deposit * mv_adjustment, total_asset),
    insolvency_2 = safe_div((total_asset - total_liability) - 1.0 * uninsured_deposit * mv_adjustment, total_asset),
    adjusted_equity = equity_ratio - mtm_loss_to_total_asset,
    mtm_insolvent = as.integer(adjusted_equity < 0) * 100,
    insolvent_idcr_s50 = as.integer(idcr_1 < 0),
    insolvent_idcr_s100 = as.integer(idcr_2 < 0),
    insolvent_cap_s50 = as.integer(insolvency_1 < 0),
    insolvent_cap_s100 = as.integer(insolvency_2 < 0)

    ) %>%
    # Interaction terms
    mutate(
      mtm_btfp_x_uninsured = mtm_btfp * uninsured_lev,
      adj_equity_x_uninsured = adjusted_equity * uninsured_lev
    )
}

## Apply to both baselines
baseline_1 <- construct_analysis_vars(baseline_1)
baseline_2 <- construct_analysis_vars(baseline_2)
baseline <- baseline_1  # Default

cat("✓ Analysis variables constructed\n")
#> ✓ Analysis variables constructed
cat("  Baseline 1:", nrow(baseline_1), "banks with", ncol(baseline_1), "variables\n")
#>   Baseline 1: 4292 banks with 77 variables
cat("  Baseline 2:", nrow(baseline_2), "banks with", ncol(baseline_2), "variables\n")
#>   Baseline 2: 4737 banks with 77 variables

1.10 Aggregate Facility Usage

# ==========================================================================
# AGGREGATE FACILITY USAGE BY BANK
# ==========================================================================

cat("\n=== AGGREGATING FACILITY USAGE ===\n")
#> 
#> === AGGREGATING FACILITY USAGE ===
## Full program period (post-BTFP launch)
btfp_full <- btfp_raw %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= BTFP_CLOSE) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp = 1L,
    btfp_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_first_date = min(btfp_loan_date),
    btfp_n_loans = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

dw_full <- dw_raw %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= BTFP_CLOSE) %>%
  group_by(rssd_id) %>%
  summarise(
    dw = 1L,
    dw_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_first_date = min(dw_loan_date),
    dw_n_loans = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

## Pre-BTFP period (baseline DW usage)
dw_pre_btfp <- dw_raw %>%
  filter(dw_loan_date >= PRE_BTFP_START, dw_loan_date <= PRE_BTFP_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_pre_btfp = 1L,
    dw_pre_btfp_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_pre_btfp_first = min(dw_loan_date),
    dw_pre_btfp_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

cat("✓ Facility usage aggregated\n")
#> ✓ Facility usage aggregated
cat("  BTFP users (full program):", nrow(btfp_full), "banks\n")
#>   BTFP users (full program): 1327 banks
cat("  DW users (post-BTFP):", nrow(dw_full), "banks\n")
#>   DW users (post-BTFP): 1092 banks
cat("  DW users (pre-BTFP):", nrow(dw_pre_btfp), "banks\n")
#>   DW users (pre-BTFP): 633 banks

1.11 Aggregate by OLD Period Definitions

# ==========================================================================
# AGGREGATE FACILITY USAGE BY OLD PERIOD DEFINITIONS
# (Acute, Post-Acute, Arbitrage)
# ==========================================================================

cat("\n=== AGGREGATING BY OLD PERIODS ===\n")
#> 
#> === AGGREGATING BY OLD PERIODS ===
## BTFP by old periods
btfp_acute_old <- btfp_raw %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= PERIOD_1_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_acute = 1L,
    btfp_acute_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_acute_first = min(btfp_loan_date),
    btfp_acute_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_post_old <- btfp_raw %>%
  filter(btfp_loan_date > PERIOD_1_END, btfp_loan_date <= PERIOD_2_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_post = 1L,
    btfp_post_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_post_first = min(btfp_loan_date),
    btfp_post_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_arb_old <- btfp_raw %>%
  filter(btfp_loan_date > PERIOD_2_END, btfp_loan_date <= PERIOD_3_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_arb = 1L,
    btfp_arb_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_arb_first = min(btfp_loan_date),
    btfp_arb_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

## DW by old periods
dw_acute_old <- dw_raw %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= PERIOD_1_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_acute = 1L,
    dw_acute_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_acute_first = min(dw_loan_date),
    dw_acute_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

dw_post_old <- dw_raw %>%
  filter(dw_loan_date > PERIOD_1_END, dw_loan_date <= PERIOD_2_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_post = 1L,
    dw_post_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_post_first = min(dw_loan_date),
    dw_post_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

cat("✓ Old period aggregation complete\n")
#> ✓ Old period aggregation complete
cat("  BTFP Acute:", nrow(btfp_acute_old), "| Post-Acute:", nrow(btfp_post_old), "| Arbitrage:", nrow(btfp_arb_old), "\n")
#>   BTFP Acute: 492 | Post-Acute: 816 | Arbitrage: 801
cat("  DW Acute:", nrow(dw_acute_old), "| Post-Acute:", nrow(dw_post_old), "\n")
#>   DW Acute: 424 | Post-Acute: 846

1.12 Aggregate by NEW Period Definitions

# ==========================================================================
# AGGREGATE FACILITY USAGE BY NEW PERIOD DEFINITIONS
# (P0-P6)
# ==========================================================================

cat("\n=== AGGREGATING BY NEW PERIODS ===\n")
#> 
#> === AGGREGATING BY NEW PERIODS ===
## BTFP by new periods
btfp_p1 <- btfp_raw %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= WEEK1_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p1 = 1L,
    btfp_p1_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p1_first = min(btfp_loan_date),
    btfp_p1_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_p2 <- btfp_raw %>%
  filter(btfp_loan_date > WEEK1_END, btfp_loan_date <= CRISIS_MONTH_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p2 = 1L,
    btfp_p2_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p2_first = min(btfp_loan_date),
    btfp_p2_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_p3 <- btfp_raw %>%
  filter(btfp_loan_date >= FRC_FAIL, btfp_loan_date <= MAY_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p3 = 1L,
    btfp_p3_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p3_first = min(btfp_loan_date),
    btfp_p3_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_p4 <- btfp_raw %>%
  filter(btfp_loan_date >= SUMMER_START, btfp_loan_date <= DW_DATA_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p4 = 1L,
    btfp_p4_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p4_first = min(btfp_loan_date),
    btfp_p4_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_p5 <- btfp_raw %>%
  filter(btfp_loan_date >= ARBITRAGE_START, btfp_loan_date <= ARBITRAGE_END) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p5 = 1L,
    btfp_p5_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p5_first = min(btfp_loan_date),
    btfp_p5_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

btfp_p6 <- btfp_raw %>%
  filter(btfp_loan_date > ARBITRAGE_END, btfp_loan_date <= BTFP_CLOSE) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_p6 = 1L,
    btfp_p6_amount = sum(btfp_loan_amount, na.rm = TRUE),
    btfp_p6_first = min(btfp_loan_date),
    btfp_p6_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

## DW by new periods (only P1-P4, no data after Sep 30)
dw_p1 <- dw_raw %>%
  filter(dw_loan_date >= CRISIS_START, dw_loan_date <= WEEK1_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_p1 = 1L,
    dw_p1_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_p1_first = min(dw_loan_date),
    dw_p1_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

dw_p2 <- dw_raw %>%
  filter(dw_loan_date > WEEK1_END, dw_loan_date <= CRISIS_MONTH_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_p2 = 1L,
    dw_p2_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_p2_first = min(dw_loan_date),
    dw_p2_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

dw_p3 <- dw_raw %>%
  filter(dw_loan_date >= FRC_FAIL, dw_loan_date <= MAY_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_p3 = 1L,
    dw_p3_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_p3_first = min(dw_loan_date),
    dw_p3_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

dw_p4 <- dw_raw %>%
  filter(dw_loan_date >= SUMMER_START, dw_loan_date <= DW_DATA_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_p4 = 1L,
    dw_p4_amount = sum(dw_loan_amount, na.rm = TRUE),
    dw_p4_first = min(dw_loan_date),
    dw_p4_n = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

cat("✓ New period aggregation complete\n")
#> ✓ New period aggregation complete
cat("  BTFP: P1=", nrow(btfp_p1), "| P2=", nrow(btfp_p2), "| P3=", nrow(btfp_p3), 
    "| P4=", nrow(btfp_p4), "| P5=", nrow(btfp_p5), "| P6=", nrow(btfp_p6), "\n")
#>   BTFP: P1= 87 | P2= 451 | P3= 424 | P4= 482 | P5= 801 | P6= 240
cat("  DW: P1=", nrow(dw_p1), "| P2=", nrow(dw_p2), "| P3=", nrow(dw_p3), "| P4=", nrow(dw_p4), "\n")
#>   DW: P1= 169 | P2= 319 | P3= 246 | P4= 728
cat("  (Note: No DW data for P5-P6)\n")
#>   (Note: No DW data for P5-P6)

1.13 Create Final Analysis Datasets

# ==========================================================================
# MERGE ALL DATA TO CREATE FINAL ANALYSIS DATASETS
# ==========================================================================

cat("\n=== CREATING FINAL ANALYSIS DATASETS ===\n")
#> 
#> === CREATING FINAL ANALYSIS DATASETS ===
## Function to create complete analysis dataset from baseline
create_analysis_dataset <- function(baseline_data, baseline_name) {
  result <- baseline_data %>%
    # Merge full program usage
    left_join(btfp_full, by = "idrssd") %>%
    left_join(dw_full, by = "idrssd") %>%
    left_join(dw_pre_btfp, by = "idrssd") %>%
    # Merge old period definitions
    left_join(btfp_acute_old, by = "idrssd") %>%
    left_join(btfp_post_old, by = "idrssd") %>%
    left_join(btfp_arb_old, by = "idrssd") %>%
    left_join(dw_acute_old, by = "idrssd") %>%
    left_join(dw_post_old, by = "idrssd") %>%
    # Merge new period definitions
    left_join(btfp_p1, by = "idrssd") %>%
    left_join(btfp_p2, by = "idrssd") %>%
    left_join(btfp_p3, by = "idrssd") %>%
    left_join(btfp_p4, by = "idrssd") %>%
    left_join(btfp_p5, by = "idrssd") %>%
    left_join(btfp_p6, by = "idrssd") %>%
    left_join(dw_p1, by = "idrssd") %>%
    left_join(dw_p2, by = "idrssd") %>%
    left_join(dw_p3, by = "idrssd") %>%
    left_join(dw_p4, by = "idrssd") %>%
    # Replace NA with 0 for binary indicators
    mutate(across(c(btfp, dw, dw_pre_btfp, 
                    btfp_acute, btfp_post, btfp_arb, dw_acute, dw_post,
                    btfp_p1, btfp_p2, btfp_p3, btfp_p4, btfp_p5, btfp_p6,
                    dw_p1, dw_p2, dw_p3, dw_p4), 
                  ~coalesce(as.integer(.), 0L))) %>%
    # Create facility choice variables
    mutate(
      both = as.integer(btfp == 1 & dw == 1),
      btfp_only = as.integer(btfp == 1 & dw == 0),
      dw_only = as.integer(btfp == 0 & dw == 1),
      neither = as.integer(btfp == 0 & dw == 0),
      any_fed = as.integer(btfp == 1 | dw == 1),
      facility = case_when(
        both == 1 ~ "Both",
        btfp_only == 1 ~ "BTFP Only",
        dw_only == 1 ~ "DW Only",
        TRUE ~ "Neither"
      ),
      facility = factor(facility, levels = c("BTFP Only", "DW Only", "Both", "Neither"))
    ) %>%
    # Add period-specific both indicators (OLD periods)
    mutate(
      both_acute = as.integer(btfp_acute == 1 & dw_acute == 1),
      both_post = as.integer(btfp_post == 1 & dw_post == 1),
      both_arb = as.integer((btfp_arb == 1 | btfp_p5 == 1 | btfp_p6 == 1) & dw_post == 1),
      # NEW period both indicators
      both_p1 = as.integer(btfp_p1 == 1 & dw_p1 == 1),
      both_p2 = as.integer(btfp_p2 == 1 & dw_p2 == 1),
      both_p3 = as.integer(btfp_p3 == 1 & dw_p3 == 1),
      both_p4 = as.integer(btfp_p4 == 1 & dw_p4 == 1)
    ) %>%
    # Borrowing amounts and percentages
    mutate(
      d_assets = assets * 1000,
      btfp_amount_pct = safe_div(btfp_amount, d_assets) * 100,
      dw_amount_pct = safe_div(dw_amount, d_assets) * 100,
      total_borrowed = btfp_amount + dw_amount,
      total_borrowed_pct = safe_div(total_borrowed, d_assets) * 100,
      
      # OLD period amounts
      btfp_acute_amount_pct = safe_div(btfp_acute_amount, d_assets) * 100,
      btfp_post_amount_pct = safe_div(btfp_post_amount, d_assets) * 100,
      btfp_arb_amount_pct = safe_div(btfp_arb_amount, d_assets) * 100,
      dw_acute_amount_pct = safe_div(dw_acute_amount, d_assets) * 100,
      dw_post_amount_pct = safe_div(dw_post_amount, d_assets) * 100,
      dw_pre_btfp_amount_pct = safe_div(dw_pre_btfp_amount, d_assets) * 100,
      
      # NEW period amounts (P1-P6)
      btfp_p1_amount_pct = safe_div(btfp_p1_amount, d_assets) * 100,
      btfp_p2_amount_pct = safe_div(btfp_p2_amount, d_assets) * 100,
      btfp_p3_amount_pct = safe_div(btfp_p3_amount, d_assets) * 100,
      btfp_p4_amount_pct = safe_div(btfp_p4_amount, d_assets) * 100,
      btfp_p5_amount_pct = safe_div(btfp_p5_amount, d_assets) * 100,
      btfp_p6_amount_pct = safe_div(btfp_p6_amount, d_assets) * 100,
      dw_p1_amount_pct = safe_div(dw_p1_amount, d_assets) * 100,
      dw_p2_amount_pct = safe_div(dw_p2_amount, d_assets) * 100,
      dw_p3_amount_pct = safe_div(dw_p3_amount, d_assets) * 100,
      
      # OLD period shares
      btfp_share_acute = ifelse(both_acute == 1,
                                safe_div(btfp_acute_amount, btfp_acute_amount + dw_acute_amount) * 100,
                                NA_real_),
      btfp_share_post = ifelse(both_post == 1,
                               safe_div(btfp_post_amount, btfp_post_amount + dw_post_amount) * 100,
                               NA_real_),
      
      # NEW period shares
      btfp_share_p1 = ifelse(both_p1 == 1,
                             safe_div(btfp_p1_amount, btfp_p1_amount + dw_p1_amount) * 100,
                             NA_real_),
      btfp_share_p2 = ifelse(both_p2 == 1,
                             safe_div(btfp_p2_amount, btfp_p2_amount + dw_p2_amount) * 100,
                             NA_real_),
      btfp_share_p3 = ifelse(both_p3 == 1,
                             safe_div(btfp_p3_amount, btfp_p3_amount + dw_p3_amount) * 100,
                             NA_real_),
      
      # Utilization Calculations
      collateral_capacity = (eligible_collateral / 100) * d_assets,
      
      # Overall Utilization
      btfp_utilization = safe_div(btfp_amount, collateral_capacity),
      maxed_out_btfp = as.integer(btfp_utilization > 0.90),
      
      # Period Utilization (OLD)
      btfp_util_acute = safe_div(btfp_acute_amount, collateral_capacity),
      btfp_util_post = safe_div(btfp_post_amount, collateral_capacity),
      btfp_util_arb = safe_div(btfp_arb_amount, collateral_capacity),
      
      # Period Utilization (NEW)
      btfp_util_p1 = safe_div(btfp_p1_amount, collateral_capacity),
      btfp_util_p2 = safe_div(btfp_p2_amount, collateral_capacity),
      btfp_util_p3 = safe_div(btfp_p3_amount, collateral_capacity),
      btfp_util_p4 = safe_div(btfp_p4_amount, collateral_capacity),
      btfp_util_p5 = safe_div(btfp_p5_amount, collateral_capacity),
      btfp_util_p6 = safe_div(btfp_p6_amount, collateral_capacity)
    )
  
  return(result)
}

## Create both datasets
df_1 <- create_analysis_dataset(baseline_1, "Baseline 1")
df_2 <- create_analysis_dataset(baseline_2, "Baseline 2")
df <- df_1  # Default dataset

cat("✓ Final analysis datasets created\n")
#> ✓ Final analysis datasets created
cat("  df_1 (Baseline 1):", nrow(df_1), "banks\n")
#>   df_1 (Baseline 1): 4292 banks
cat("  df_2 (Baseline 2):", nrow(df_2), "banks\n")
#>   df_2 (Baseline 2): 4737 banks
cat("\n")
cat("Facility usage summary (Baseline 1):\n")
#> Facility usage summary (Baseline 1):
print(table(df_1$facility))
#> 
#> BTFP Only   DW Only      Both   Neither 
#>       846       591       413      2442
cat("\n")
cat("Facility usage summary (Baseline 2):\n")
#> Facility usage summary (Baseline 2):
print(table(df_2$facility))
#> 
#> BTFP Only   DW Only      Both   Neither 
#>       881       647       435      2774
cat("\n")
cat(strrep("=", 80), "\n")
#> ================================================================================
cat("✓✓✓ PART I COMPLETE: Setup, Data Loading, and Period Definitions ✓✓✓\n")
#> ✓✓✓ PART I COMPLETE: Setup, Data Loading, and Period Definitions ✓✓✓
cat(strrep("=", 80), "\n")
#> ================================================================================

PART II: DESCRIPTIVE STATISTICS AND SUMMARY TABLES

This section provides comprehensive descriptive statistics organized by: - Section 2.1: Loan-level statistics by OLD period definitions (all banks vs. baseline 1) - Section 2.2: Loan-level statistics by NEW period definitions (P0-P6) - Section 2.3: Bank-level characteristics by facility choice - Section 2.4: Correlation matrices - Section 2.5: Balance sheet composition (2022Q4)

2.1 Loan-Level Statistics by OLD Period Definitions

2.1.1 Discount Window (DW) - All Banks

# ==========================================================================
# DW LOAN STATISTICS BY OLD PERIODS - ALL BANKS
# ==========================================================================

cat("\n=== DW LOAN STATISTICS (OLD PERIODS, ALL BANKS) ===\n")
#> 
#> === DW LOAN STATISTICS (OLD PERIODS, ALL BANKS) ===
## Variable configuration
vars_dw <- list(
  "dw_loan_amount"      = "Loan Amount (Millions)",
  "dw_interest_rate"    = "Interest Rate (%)",
  "dw_term"             = "Term (Days)",
  "dw_total_collateral" = "Collateral Pledged (Millions)",
  "utilization"         = "Utilization (%)",
  "pct_omo"             = "BTFP Eligible Share (%)"
)

## Helper function
get_period_stats_old <- function(data, period_name, var_list, facility_type = "DW") {
  
  df_period <- data %>% filter(period_old == period_name)
  
  if (nrow(df_period) == 0) {
    return(tibble(Variable = character(), N = integer(), Total_B = numeric(),
                  Mean = numeric(), Median = numeric(), SD = numeric(),
                  Min = numeric(), Max = numeric()))
  }
  
  n_banks <- n_distinct(df_period$rssd_id)
  amt_col <- if(facility_type == "DW") "dw_loan_amount" else "btfp_loan_amount"
  total_amount <- sum(df_period[[amt_col]], na.rm = TRUE) / 1e9
  
  row_unique <- tibble(
    Variable = "Unique Borrowers (Count)",
    N = n_banks, Total_B = NA, Mean = NA, Median = NA, SD = NA, Min = NA, Max = NA
  )
  
  row_total <- tibble(
    Variable = "Period Issuance (Billions)",
    N = nrow(df_period), Total_B = total_amount, Mean = NA, Median = NA, SD = NA, Min = NA, Max = NA
  )
  
  stats_rows <- purrr::map_dfr(names(var_list), function(v_col) {
    vals <- df_period[[v_col]]
    is_money <- str_detect(v_col, "amount|collateral")
    total_val <- if(is_money) sum(vals, na.rm = TRUE) / 1e9 else NA
    calc_vals <- if(is_money) vals / 1e6 else vals
    
    tibble(
      Variable = var_list[[v_col]],
      N      = sum(!is.na(vals)),
      Total_B = total_val,
      Mean   = mean(calc_vals, na.rm = TRUE),
      Median = median(calc_vals, na.rm = TRUE),
      SD     = sd(calc_vals, na.rm = TRUE),
      Min    = min(calc_vals, na.rm = TRUE),
      Max    = max(calc_vals, na.rm = TRUE)
    )
  })
  
  bind_rows(row_unique, row_total, stats_rows)
}

## Prepare DW data with calculated variables
dw_ready_all <- dw_loans %>%
  mutate(
    utilization = (dw_loan_amount / dw_total_collateral) * 100,
    pct_omo = (dw_omo_eligible / dw_total_collateral) * 100
  )

## Generate statistics for each old period
dw_stats_prebtfp <- get_period_stats_old(dw_ready_all, "Pre-BTFP", vars_dw, "DW")
dw_stats_acute <- get_period_stats_old(dw_ready_all, "Acute", vars_dw, "DW")
dw_stats_post <- get_period_stats_old(dw_ready_all, "Post-Acute", vars_dw, "DW")

dw_table_old_all <- bind_rows(dw_stats_prebtfp, dw_stats_acute, dw_stats_post)

n_prebtfp <- nrow(dw_stats_prebtfp)
n_acute <- nrow(dw_stats_acute)
n_post <- nrow(dw_stats_post)

dw_table_old_all %>%
  kbl(caption = "Table 2.1A: Discount Window Statistics by OLD Periods (All Banks)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  pack_rows("Pre-BTFP (Oct 2022 - Mar 12, 2023)", 1, n_prebtfp) %>%
  pack_rows("Acute Crisis (Mar 13 - May 1, 2023)", n_prebtfp + 1, n_prebtfp + n_acute) %>%
  pack_rows("Post-Acute (May 2 - Oct 31, 2023)", n_prebtfp + n_acute + 1, n_prebtfp + n_acute + n_post) %>%
  footnote(general = "Mean/Median/Min/Max in millions for monetary variables. All available banks included.")
Table 2.1A: Discount Window Statistics by OLD Periods (All Banks)
Variable N Total ($B) Mean Median SD Min Max
Pre-BTFP (Oct 2022 - Mar 12, 2023)
Unique Borrowers (Count) 1375 NA NA NA NA NA NA
Period Issuance (Billions) 14363 557.07 NA NA NA NA NA
Loan Amount (Millions) 14363 557.07 38.79 5.00 265.84 0.00 20377.61
Interest Rate (%) 14363 NA 1.70 0.25 1.77 0.05 4.75
Term (Days) 14363 NA 5.88 1.00 14.81 1.00 91.00
Collateral Pledged (Millions) 14363 10566.42 735.67 57.29 9278.58 0.00 312783.12
Utilization (%) 14363 NA 17.86 9.25 22.21 0.00 100.00
BTFP Eligible Share (%) 14363 NA 25.34 0.00 41.48 0.00 100.00
Acute Crisis (Mar 13 - May 1, 2023)
Unique Borrowers (Count) 424 NA NA NA NA NA NA
Period Issuance (Billions) 1619 3133.48 NA NA NA NA NA
Loan Amount (Millions) 1619 3133.48 1935.44 10.00 12075.23 0.00 127500.00
Interest Rate (%) 1619 NA 4.92 5.00 0.12 4.75 5.00
Term (Days) 1619 NA 4.51 1.00 12.73 1.00 91.00
Collateral Pledged (Millions) 1619 4745.72 2931.27 81.90 15706.74 0.00 272872.11
Utilization (%) 1619 NA 28.78 17.78 30.12 0.00 242.97
BTFP Eligible Share (%) 1619 NA 36.74 0.00 46.28 0.00 100.00
Post-Acute (May 2 - Oct 31, 2023)
Unique Borrowers (Count) 846 NA NA NA NA NA NA
Period Issuance (Billions) 4237 111.55 NA NA NA NA NA
Loan Amount (Millions) 4237 111.55 26.33 5.40 99.01 0.00 5600.00
Interest Rate (%) 4237 NA 5.35 5.25 0.13 4.95 6.00
Term (Days) 4237 NA 5.44 1.00 13.58 1.00 92.00
Collateral Pledged (Millions) 4237 2457.74 580.07 59.93 7515.33 0.00 309683.66
Utilization (%) 4237 NA 22.57 14.39 23.61 0.00 100.00
BTFP Eligible Share (%) 4237 NA 34.35 0.00 45.70 0.00 100.00
Note:
Mean/Median/Min/Max in millions for monetary variables. All available banks included.

2.1.2 BTFP - All Banks

# ==========================================================================
# BTFP LOAN STATISTICS BY OLD PERIODS - ALL BANKS
# ==========================================================================

cat("\n=== BTFP LOAN STATISTICS (OLD PERIODS, ALL BANKS) ===\n")
#> 
#> === BTFP LOAN STATISTICS (OLD PERIODS, ALL BANKS) ===
vars_btfp <- list(
  "btfp_loan_amount"      = "Loan Amount (Millions)",
  "btfp_interest_rate"    = "Interest Rate (%)",
  "btfp_term"             = "Term (Days)",
  "btfp_total_collateral" = "Collateral Pledged (Millions)",
  "utilization"           = "Utilization (%)",
  "pct_tsy"               = "Treasury Share (%)"
)

## Prepare BTFP data
btfp_ready_all <- btfp_loans %>%
  mutate(
    utilization = (btfp_loan_amount / btfp_total_collateral) * 100,
    pct_tsy = (btfp_treasury_sec / btfp_total_collateral) * 100
  )

## Generate statistics
btfp_stats_acute <- get_period_stats_old(btfp_ready_all, "Acute", vars_btfp, "BTFP")
btfp_stats_post <- get_period_stats_old(btfp_ready_all, "Post-Acute", vars_btfp, "BTFP")
btfp_stats_arb <- get_period_stats_old(btfp_ready_all, "Arbitrage", vars_btfp, "BTFP")

btfp_table_old_all <- bind_rows(btfp_stats_acute, btfp_stats_post, btfp_stats_arb)

b_acute <- nrow(btfp_stats_acute)
b_post <- nrow(btfp_stats_post)
b_arb <- nrow(btfp_stats_arb)

btfp_table_old_all %>%
  kbl(caption = "Table 2.1B: BTFP Statistics by OLD Periods (All Banks)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  pack_rows("Acute Crisis (Mar 13 - May 1, 2023)", 1, b_acute) %>%
  pack_rows("Post-Acute (May 2 - Oct 31, 2023)", b_acute + 1, b_acute + b_post) %>%
  pack_rows("Arbitrage (Nov 1, 2023 - Jan 24, 2024)", b_acute + b_post + 1, b_acute + b_post + b_arb) %>%
  footnote(general = "Mean/Median/Min/Max in millions for monetary variables. All available banks included.")
Table 2.1B: BTFP Statistics by OLD Periods (All Banks)
Variable N Total ($B) Mean Median SD Min Max
Acute Crisis (Mar 13 - May 1, 2023)
Unique Borrowers (Count) 492 NA NA NA NA NA NA
Period Issuance (Billions) 1087 145.33 NA NA NA NA NA
Loan Amount (Millions) 1087 145.33 133.70 14.00 658.38 0.00 8900.00
Interest Rate (%) 1087 NA 4.69 4.70 0.20 4.37 5.03
Term (Days) 1087 NA 311.66 365.00 125.27 1.00 369.00
Collateral Pledged (Millions) 1087 390.33 359.09 34.00 1626.91 0.01 17598.44
Utilization (%) 1087 NA 52.45 49.78 35.05 0.00 100.00
Treasury Share (%) 1087 NA 25.43 0.00 38.15 0.00 100.00
Post-Acute (May 2 - Oct 31, 2023)
Unique Borrowers (Count) 816 NA NA NA NA NA NA
Period Issuance (Billions) 1972 51.38 NA NA NA NA NA
Loan Amount (Millions) 1972 51.38 26.06 5.00 103.68 0.00 3500.00
Interest Rate (%) 1972 NA 5.20 5.35 0.31 4.37 5.59
Term (Days) 1972 NA 272.37 365.00 154.72 1.00 369.00
Collateral Pledged (Millions) 1972 556.95 282.43 26.11 2763.23 0.00 53797.57
Utilization (%) 1972 NA 34.72 22.72 32.35 0.00 100.00
Treasury Share (%) 1972 NA 24.08 0.00 36.61 0.00 100.00
Arbitrage (Nov 1, 2023 - Jan 24, 2024)
Unique Borrowers (Count) 801 NA NA NA NA NA NA
Period Issuance (Billions) 3279 229.81 NA NA NA NA NA
Loan Amount (Millions) 3279 229.81 70.09 15.00 259.09 0.00 3900.00
Interest Rate (%) 3279 NA 4.97 4.89 0.19 4.76 5.49
Term (Days) 3279 NA 334.63 365.00 97.12 1.00 369.00
Collateral Pledged (Millions) 3279 744.68 227.11 41.10 1470.39 0.01 52925.81
Utilization (%) 3279 NA 49.62 42.81 33.64 0.00 100.00
Treasury Share (%) 3279 NA 19.79 0.00 31.43 0.00 100.00
Note:
Mean/Median/Min/Max in millions for monetary variables. All available banks included.

2.1.3 DW and BTFP - Baseline 1 Only (Excl. Failed & GSIB)

# ==========================================================================
# LOAN STATISTICS BY OLD PERIODS - BASELINE 1 ONLY
# ==========================================================================

cat("\n=== LOAN STATISTICS (OLD PERIODS, BASELINE 1) ===\n")
#> 
#> === LOAN STATISTICS (OLD PERIODS, BASELINE 1) ===
## Get list of banks to exclude
excluded_banks <- call_q %>%
  filter(quarter == "2022Q4") %>%
  filter(failed_bank == 1 | gsib == 1) %>%
  pull(idrssd) %>%
  as.character()

## Filter loan data
dw_loans_b1 <- dw_loans %>%
  filter(!rssd_id %in% excluded_banks) %>%
  mutate(
    utilization = (dw_loan_amount / dw_total_collateral) * 100,
    pct_omo = (dw_omo_eligible / dw_total_collateral) * 100
  )

btfp_loans_b1 <- btfp_loans %>%
  filter(!rssd_id %in% excluded_banks) %>%
  mutate(
    utilization = (btfp_loan_amount / btfp_total_collateral) * 100,
    pct_tsy = (btfp_treasury_sec / btfp_total_collateral) * 100
  )

cat("Excluded", length(excluded_banks), "banks (failed + GSIB)\n")
#> Excluded 41 banks (failed + GSIB)
cat("DW loans: Baseline 1 =", nrow(dw_loans_b1), "\n")
#> DW loans: Baseline 1 = 20018
cat("BTFP loans: Baseline 1 =", nrow(btfp_loans_b1), "\n")
#> BTFP loans: Baseline 1 = 6695
## DW Statistics
dw_b1_prebtfp <- get_period_stats_old(dw_loans_b1, "Pre-BTFP", vars_dw, "DW")
dw_b1_acute <- get_period_stats_old(dw_loans_b1, "Acute", vars_dw, "DW")
dw_b1_post <- get_period_stats_old(dw_loans_b1, "Post-Acute", vars_dw, "DW")

dw_table_old_b1 <- bind_rows(dw_b1_prebtfp, dw_b1_acute, dw_b1_post)

n_pre_b1 <- nrow(dw_b1_prebtfp)
n_acute_b1 <- nrow(dw_b1_acute)
n_post_b1 <- nrow(dw_b1_post)

dw_table_old_b1 %>%
  kbl(caption = "Table 2.1C: DW Statistics by OLD Periods (Baseline 1: Excl. Failed & GSIB)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  pack_rows("Pre-BTFP", 1, n_pre_b1) %>%
  pack_rows("Acute Crisis", n_pre_b1 + 1, n_pre_b1 + n_acute_b1) %>%
  pack_rows("Post-Acute", n_pre_b1 + n_acute_b1 + 1, n_pre_b1 + n_acute_b1 + n_post_b1)
Table 2.1C: DW Statistics by OLD Periods (Baseline 1: Excl. Failed & GSIB)
Variable N Total ($B) Mean Median SD Min Max
Pre-BTFP
Unique Borrowers (Count) 1352 NA NA NA NA NA NA
Period Issuance (Billions) 14216 429.09 NA NA NA NA NA
Loan Amount (Millions) 14216 429.09 30.18 5.00 80.75 0.00 3000.00
Interest Rate (%) 14216 NA 1.71 0.25 1.78 0.05 4.75
Term (Days) 14216 NA 5.81 1.00 14.60 1.00 91.00
Collateral Pledged (Millions) 14216 4732.32 332.89 56.18 1639.29 0.00 61371.21
Utilization (%) 14216 NA 17.85 9.29 22.06 0.00 100.00
BTFP Eligible Share (%) 14216 NA 25.28 0.00 41.52 0.00 100.00
Acute Crisis
Unique Borrowers (Count) 417 NA NA NA NA NA NA
Period Issuance (Billions) 1581 523.28 NA NA NA NA NA
Loan Amount (Millions) 1581 523.28 330.98 9.70 2102.86 0.00 25000.00
Interest Rate (%) 1581 NA 4.92 5.00 0.11 4.75 5.00
Term (Days) 1581 NA 4.51 1.00 12.67 1.00 91.00
Collateral Pledged (Millions) 1581 1327.46 839.63 74.95 3630.20 0.00 63919.59
Utilization (%) 1581 NA 27.50 17.20 28.20 0.00 100.00
BTFP Eligible Share (%) 1581 NA 37.18 0.00 46.62 0.00 100.00
Post-Acute
Unique Borrowers (Count) 836 NA NA NA NA NA NA
Period Issuance (Billions) 4221 111.53 NA NA NA NA NA
Loan Amount (Millions) 4221 111.53 26.42 5.50 99.19 0.00 5600.00
Interest Rate (%) 4221 NA 5.35 5.25 0.13 4.95 6.00
Term (Days) 4221 NA 5.46 1.00 13.60 1.00 92.00
Collateral Pledged (Millions) 4221 1426.42 337.93 59.88 2054.04 0.00 53658.43
Utilization (%) 4221 NA 22.65 14.41 23.62 0.00 100.00
BTFP Eligible Share (%) 4221 NA 34.36 0.00 45.72 0.00 100.00
## BTFP Statistics
btfp_b1_acute <- get_period_stats_old(btfp_loans_b1, "Acute", vars_btfp, "BTFP")
btfp_b1_post <- get_period_stats_old(btfp_loans_b1, "Post-Acute", vars_btfp, "BTFP")
btfp_b1_arb <- get_period_stats_old(btfp_loans_b1, "Arbitrage", vars_btfp, "BTFP")

btfp_table_old_b1 <- bind_rows(btfp_b1_acute, btfp_b1_post, btfp_b1_arb)

b1_acute <- nrow(btfp_b1_acute)
b1_post <- nrow(btfp_b1_post)
b1_arb <- nrow(btfp_b1_arb)

btfp_table_old_b1 %>%
  kbl(caption = "Table 2.1D: BTFP Statistics by OLD Periods (Baseline 1: Excl. Failed & GSIB)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  pack_rows("Acute Crisis", 1, b1_acute) %>%
  pack_rows("Post-Acute", b1_acute + 1, b1_acute + b1_post) %>%
  pack_rows("Arbitrage", b1_acute + b1_post + 1, b1_acute + b1_post + b1_arb)
Table 2.1D: BTFP Statistics by OLD Periods (Baseline 1: Excl. Failed & GSIB)
Variable N Total ($B) Mean Median SD Min Max
Acute Crisis
Unique Borrowers (Count) 485 NA NA NA NA NA NA
Period Issuance (Billions) 1071 121.13 NA NA NA NA NA
Loan Amount (Millions) 1071 121.13 113.10 14.00 571.28 0.00 8900.00
Interest Rate (%) 1071 NA 4.69 4.71 0.20 4.37 5.03
Term (Days) 1071 NA 312.75 365.00 124.31 1.00 369.00
Collateral Pledged (Millions) 1071 287.97 268.88 34.00 1278.72 0.01 17598.44
Utilization (%) 1071 NA 52.56 49.81 34.93 0.00 100.00
Treasury Share (%) 1071 NA 25.53 0.00 38.13 0.00 100.00
Post-Acute
Unique Borrowers (Count) 811 NA NA NA NA NA NA
Period Issuance (Billions) 1961 51.30 NA NA NA NA NA
Loan Amount (Millions) 1961 51.30 26.16 5.00 103.95 0.00 3500.00
Interest Rate (%) 1961 NA 5.20 5.35 0.31 4.37 5.59
Term (Days) 1961 NA 273.71 365.00 153.93 1.00 369.00
Collateral Pledged (Millions) 1961 290.38 148.08 26.09 700.99 0.00 19173.96
Utilization (%) 1961 NA 34.78 22.73 32.35 0.00 100.00
Treasury Share (%) 1961 NA 23.91 0.00 36.54 0.00 100.00
Arbitrage
Unique Borrowers (Count) 797 NA NA NA NA NA NA
Period Issuance (Billions) 3272 224.81 NA NA NA NA NA
Loan Amount (Millions) 3272 224.81 68.71 15.00 253.20 0.00 3900.00
Interest Rate (%) 3272 NA 4.97 4.89 0.19 4.76 5.49
Term (Days) 3272 NA 335.01 365.00 96.52 1.00 369.00
Collateral Pledged (Millions) 3272 632.76 193.39 41.05 692.18 0.01 18788.69
Utilization (%) 3272 NA 49.62 42.73 33.63 0.00 100.00
Treasury Share (%) 3272 NA 19.75 0.00 31.39 0.00 100.00

2.2 Loan-Level Statistics by NEW Period Definitions

2.2.1 DW Statistics by NEW Periods (P0-P4)

# ==========================================================================
# DW STATISTICS BY NEW PERIOD DEFINITIONS (P0-P4)
# ==========================================================================

cat("\n=== DW STATISTICS BY NEW PERIODS ===\n")
#> 
#> === DW STATISTICS BY NEW PERIODS ===
## Assign new periods to DW loans (Baseline 1)
dw_loans_new <- dw_loans_b1 %>%
  mutate(
    period_new = case_when(
      dw_loan_date >= PRE_BTFP_START & dw_loan_date <= PRE_BTFP_END ~ "P0: Pre-BTFP",
      dw_loan_date >= CRISIS_START & dw_loan_date <= WEEK1_END ~ "P1: Week 1",
      dw_loan_date > WEEK1_END & dw_loan_date <= CRISIS_MONTH_END ~ "P2: Crisis Month",
      dw_loan_date > CRISIS_MONTH_END & dw_loan_date <= MAY_END ~ "P3: FRC May",
      dw_loan_date > MAY_END & dw_loan_date <= DW_DATA_END ~ "P4: Stabilization",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(period_new))

## Helper function for new periods
get_period_stats_new <- function(data, period_name, var_list, facility_type = "DW") {
  
  df_period <- data %>% filter(period_new == period_name)
  
  if (nrow(df_period) == 0) {
    return(tibble(Variable = character(), N = integer(), Total_B = numeric(),
                  Mean = numeric(), Median = numeric(), SD = numeric(),
                  Min = numeric(), Max = numeric()))
  }
  
  n_banks <- n_distinct(df_period$rssd_id)
  amt_col <- if(facility_type == "DW") "dw_loan_amount" else "btfp_loan_amount"
  total_amount <- sum(df_period[[amt_col]], na.rm = TRUE) / 1e9
  
  row_unique <- tibble(
    Variable = "Unique Borrowers (Count)",
    N = n_banks, Total_B = NA, Mean = NA, Median = NA, SD = NA, Min = NA, Max = NA
  )
  
  row_total <- tibble(
    Variable = "Period Issuance (Billions)",
    N = nrow(df_period), Total_B = total_amount, Mean = NA, Median = NA, SD = NA, Min = NA, Max = NA
  )
  
  stats_rows <- purrr::map_dfr(names(var_list), function(v_col) {
    vals <- df_period[[v_col]]
    is_money <- str_detect(v_col, "amount|collateral")
    total_val <- if(is_money) sum(vals, na.rm = TRUE) / 1e9 else NA
    calc_vals <- if(is_money) vals / 1e6 else vals
    
    tibble(
      Variable = var_list[[v_col]],
      N      = sum(!is.na(vals)),
      Total_B = total_val,
      Mean   = mean(calc_vals, na.rm = TRUE),
      Median = median(calc_vals, na.rm = TRUE),
      SD     = sd(calc_vals, na.rm = TRUE),
      Min    = min(calc_vals, na.rm = TRUE),
      Max    = max(calc_vals, na.rm = TRUE)
    )
  })
  
  bind_rows(row_unique, row_total, stats_rows)
}

## Generate statistics
dw_p0 <- get_period_stats_new(dw_loans_new, "P0: Pre-BTFP", vars_dw, "DW")
dw_p1 <- get_period_stats_new(dw_loans_new, "P1: Week 1", vars_dw, "DW")
dw_p2 <- get_period_stats_new(dw_loans_new, "P2: Crisis Month", vars_dw, "DW")
dw_p3 <- get_period_stats_new(dw_loans_new, "P3: FRC May", vars_dw, "DW")
dw_p4 <- get_period_stats_new(dw_loans_new, "P4: Stabilization", vars_dw, "DW")

dw_new_table <- bind_rows(dw_p0, dw_p1, dw_p2, dw_p3, dw_p4)

n0 <- nrow(dw_p0)
n1 <- nrow(dw_p1)
n2 <- nrow(dw_p2)
n3 <- nrow(dw_p3)
n4 <- nrow(dw_p4)

dw_new_table %>%
  kbl(caption = "Table 2.2A: DW Statistics by NEW Period Definitions (Baseline 1)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("P0: Pre-BTFP (Oct 1, 2022 - Mar 12, 2023)", 1, n0) %>%
  pack_rows("P1: Week 1 (Mar 13-19, 2023)", n0 + 1, n0 + n1) %>%
  pack_rows("P2: Crisis Month (Mar 20 - Apr 30, 2023)", n0 + n1 + 1, n0 + n1 + n2) %>%
  pack_rows("P3: FRC May (May 1-31, 2023)", n0 + n1 + n2 + 1, n0 + n1 + n2 + n3) %>%
  pack_rows("P4: Stabilization (Jun 1 - Sep 30, 2023)", n0 + n1 + n2 + n3 + 1, n0 + n1 + n2 + n3 + n4) %>%
  footnote(general = "DW data ends Sep 30, 2023. Baseline 1 excludes failed banks and GSIBs.")
Table 2.2A: DW Statistics by NEW Period Definitions (Baseline 1)
Variable N Total ($B) Mean Median SD Min Max
P0: Pre-BTFP (Oct 1, 2022 - Mar 12, 2023)
Unique Borrowers (Count) 616 NA NA NA NA NA NA
Period Issuance (Billions) 4149 156.65 NA NA NA NA NA
Loan Amount (Millions) 4149 156.65 37.76 9.00 80.26 0.00 2000.00
Interest Rate (%) 4149 NA 4.22 4.50 0.51 2.85 4.75
Term (Days) 4149 NA 4.86 1.00 12.27 1.00 90.00
Collateral Pledged (Millions) 4149 1262.46 304.28 69.57 1582.86 0.00 61371.21
Utilization (%) 4149 NA 21.60 13.85 22.00 0.00 100.00
BTFP Eligible Share (%) 4149 NA 29.78 0.00 44.51 0.00 100.00
P1: Week 1 (Mar 13-19, 2023)
Unique Borrowers (Count) 164 NA NA NA NA NA NA
Period Issuance (Billions) 319 211.00 NA NA NA NA NA
Loan Amount (Millions) 319 211.00 661.45 10.00 3428.59 0.00 25000.00
Interest Rate (%) 319 NA 4.75 4.75 0.00 4.75 4.75
Term (Days) 319 NA 6.64 1.00 17.32 1.00 91.00
Collateral Pledged (Millions) 319 357.42 1120.43 112.82 4171.85 0.04 30543.65
Utilization (%) 319 NA 24.97 13.02 27.78 0.00 100.00
BTFP Eligible Share (%) 319 NA 27.57 0.00 41.95 0.00 100.00
P2: Crisis Month (Mar 20 - Apr 30, 2023)
Unique Borrowers (Count) 315 NA NA NA NA NA NA
Period Issuance (Billions) 1220 310.06 NA NA NA NA NA
Loan Amount (Millions) 1220 310.06 254.15 9.62 1620.88 0.00 20000.00
Interest Rate (%) 1220 NA 4.97 5.00 0.08 4.75 5.00
Term (Days) 1220 NA 3.96 1.00 11.02 1.00 91.00
Collateral Pledged (Millions) 1220 954.68 782.52 67.07 3534.57 0.00 63919.59
Utilization (%) 1220 NA 28.16 18.41 28.26 0.00 99.99
BTFP Eligible Share (%) 1220 NA 39.58 0.03 47.40 0.00 100.00
P3: FRC May (May 1-31, 2023)
Unique Borrowers (Count) 240 NA NA NA NA NA NA
Period Issuance (Billions) 872 41.22 NA NA NA NA NA
Loan Amount (Millions) 872 41.22 47.27 8.62 206.33 0.00 5600.00
Interest Rate (%) 872 NA 5.21 5.25 0.09 4.95 5.25
Term (Days) 872 NA 4.94 1.00 13.34 1.00 90.00
Collateral Pledged (Millions) 872 343.08 393.45 65.58 1825.85 0.00 43843.70
Utilization (%) 872 NA 23.38 14.80 24.10 0.00 100.00
BTFP Eligible Share (%) 872 NA 38.39 0.52 47.26 0.00 100.00
P4: Stabilization (Jun 1 - Sep 30, 2023)
Unique Borrowers (Count) 721 NA NA NA NA NA NA
Period Issuance (Billions) 3391 72.52 NA NA NA NA NA
Loan Amount (Millions) 3391 72.52 21.39 5.00 37.37 0.00 460.00
Interest Rate (%) 3391 NA 5.38 5.50 0.12 5.25 6.00
Term (Days) 3391 NA 5.58 1.00 13.68 1.00 92.00
Collateral Pledged (Millions) 3391 1098.70 324.01 55.29 2097.69 0.00 53658.43
Utilization (%) 3391 NA 22.52 14.40 23.57 0.00 99.96
BTFP Eligible Share (%) 3391 NA 33.40 0.00 45.31 0.00 100.00
Note:
DW data ends Sep 30, 2023. Baseline 1 excludes failed banks and GSIBs.

2.2.2 BTFP Statistics by NEW Periods (P1-P6)

# ==========================================================================
# BTFP STATISTICS BY NEW PERIOD DEFINITIONS (P1-P6)
# ==========================================================================

cat("\n=== BTFP STATISTICS BY NEW PERIODS ===\n")
#> 
#> === BTFP STATISTICS BY NEW PERIODS ===
## Assign new periods to BTFP loans (Baseline 1)
btfp_loans_new <- btfp_loans_b1 %>%
  mutate(
    period_new = case_when(
      btfp_loan_date >= CRISIS_START & btfp_loan_date <= WEEK1_END ~ "P1: Week 1",
      btfp_loan_date > WEEK1_END & btfp_loan_date <= CRISIS_MONTH_END ~ "P2: Crisis Month",
      btfp_loan_date > CRISIS_MONTH_END & btfp_loan_date <= MAY_END ~ "P3: FRC May",
      btfp_loan_date > MAY_END & btfp_loan_date <= DW_DATA_END ~ "P4: Stabilization",
      btfp_loan_date >= ARBITRAGE_START & btfp_loan_date <= ARBITRAGE_END ~ "P5: Arbitrage",
      btfp_loan_date > ARBITRAGE_END & btfp_loan_date <= BTFP_CLOSE ~ "P6: Wind-down",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(period_new))

## Generate statistics
btfp_p1 <- get_period_stats_new(btfp_loans_new, "P1: Week 1", vars_btfp, "BTFP")
btfp_p2 <- get_period_stats_new(btfp_loans_new, "P2: Crisis Month", vars_btfp, "BTFP")
btfp_p3 <- get_period_stats_new(btfp_loans_new, "P3: FRC May", vars_btfp, "BTFP")
btfp_p4 <- get_period_stats_new(btfp_loans_new, "P4: Stabilization", vars_btfp, "BTFP")
btfp_p5 <- get_period_stats_new(btfp_loans_new, "P5: Arbitrage", vars_btfp, "BTFP")
btfp_p6 <- get_period_stats_new(btfp_loans_new, "P6: Wind-down", vars_btfp, "BTFP")

btfp_new_table <- bind_rows(btfp_p1, btfp_p2, btfp_p3, btfp_p4, btfp_p5, btfp_p6)

b1 <- nrow(btfp_p1)
b2 <- nrow(btfp_p2)
b3 <- nrow(btfp_p3)
b4 <- nrow(btfp_p4)
b5 <- nrow(btfp_p5)
b6 <- nrow(btfp_p6)

btfp_new_table %>%
  kbl(caption = "Table 2.2B: BTFP Statistics by NEW Period Definitions (Baseline 1)",
      digits = 2,
      col.names = c("Variable", "N", "Total ($B)", "Mean", "Median", "SD", "Min", "Max")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE, font_size = 10) %>%
  pack_rows("P1: Week 1 (Mar 13-19, 2023)", 1, b1) %>%
  pack_rows("P2: Crisis Month (Mar 20 - Apr 30, 2023)", b1 + 1, b1 + b2) %>%
  pack_rows("P3: FRC May (May 1-31, 2023)", b1 + b2 + 1, b1 + b2 + b3) %>%
  pack_rows("P4: Stabilization (Jun 1 - Sep 30, 2023)", b1 + b2 + b3 + 1, b1 + b2 + b3 + b4) %>%
  pack_rows("P5: Arbitrage (Nov 1, 2023 - Jan 24, 2024)", b1 + b2 + b3 + b4 + 1, b1 + b2 + b3 + b4 + b5) %>%
  pack_rows("P6: Wind-down (Jan 25 - Mar 11, 2024)", b1 + b2 + b3 + b4 + b5 + 1, b1 + b2 + b3 + b4 + b5 + b6) %>%
  footnote(general = "Baseline 1 excludes failed banks and GSIBs. P5-P6 have no DW comparison data.")
Table 2.2B: BTFP Statistics by NEW Period Definitions (Baseline 1)
Variable N Total ($B) Mean Median SD Min Max
P1: Week 1 (Mar 13-19, 2023)
Unique Borrowers (Count) 84 NA NA NA NA NA NA
Period Issuance (Billions) 98 17.01 NA NA NA NA NA
Loan Amount (Millions) 98 17.01 173.56 20.00 475.43 0.00 4000.00
Interest Rate (%) 98 NA 4.57 4.50 0.13 4.39 4.83
Term (Days) 98 NA 297.82 365.00 140.49 1.00 368.00
Collateral Pledged (Millions) 98 50.12 511.39 45.96 1451.90 1.00 11143.50
Utilization (%) 98 NA 63.20 79.31 34.76 0.00 100.00
Treasury Share (%) 98 NA 22.73 0.00 36.71 0.00 100.00
P2: Crisis Month (Mar 20 - Apr 30, 2023)
Unique Borrowers (Count) 446 NA NA NA NA NA NA
Period Issuance (Billions) 949 103.85 NA NA NA NA NA
Loan Amount (Millions) 949 103.85 109.43 14.00 587.00 0.00 8900.00
Interest Rate (%) 949 NA 4.69 4.76 0.20 4.37 5.03
Term (Days) 949 NA 314.54 365.00 122.21 1.00 369.00
Collateral Pledged (Millions) 949 236.65 249.37 34.00 1273.59 0.01 17598.44
Utilization (%) 949 NA 51.72 48.10 34.70 0.00 100.00
Treasury Share (%) 949 NA 25.44 0.00 38.07 0.00 100.00
P3: FRC May (May 1-31, 2023)
Unique Borrowers (Count) 421 NA NA NA NA NA NA
Period Issuance (Billions) 759 23.61 NA NA NA NA NA
Loan Amount (Millions) 759 23.61 31.11 10.00 80.42 0.00 1000.00
Interest Rate (%) 759 NA 4.84 4.80 0.16 4.70 5.22
Term (Days) 759 NA 315.07 365.00 121.24 1.00 369.00
Collateral Pledged (Millions) 759 121.15 159.62 29.00 761.63 0.09 16160.57
Utilization (%) 759 NA 41.66 33.33 33.53 0.00 100.00
Treasury Share (%) 759 NA 24.30 0.00 36.09 0.00 100.00
P4: Stabilization (Jun 1 - Sep 30, 2023)
Unique Borrowers (Count) 478 NA NA NA NA NA NA
Period Issuance (Billions) 1026 22.53 NA NA NA NA NA
Loan Amount (Millions) 1026 22.53 21.96 4.00 123.11 0.00 3500.00
Interest Rate (%) 1026 NA 5.41 5.45 0.13 4.37 5.59
Term (Days) 1026 NA 243.57 365.00 167.91 1.00 369.00
Collateral Pledged (Millions) 1026 122.39 119.28 21.83 370.62 0.00 6664.42
Utilization (%) 1026 NA 30.92 19.96 31.00 0.00 100.00
Treasury Share (%) 1026 NA 24.13 0.00 37.10 0.00 100.00
P5: Arbitrage (Nov 1, 2023 - Jan 24, 2024)
Unique Borrowers (Count) 797 NA NA NA NA NA NA
Period Issuance (Billions) 3272 224.81 NA NA NA NA NA
Loan Amount (Millions) 3272 224.81 68.71 15.00 253.20 0.00 3900.00
Interest Rate (%) 3272 NA 4.97 4.89 0.19 4.76 5.49
Term (Days) 3272 NA 335.01 365.00 96.52 1.00 369.00
Collateral Pledged (Millions) 3272 632.76 193.39 41.05 692.18 0.01 18788.69
Utilization (%) 3272 NA 49.62 42.73 33.63 0.00 100.00
Treasury Share (%) 3272 NA 19.75 0.00 31.39 0.00 100.00
P6: Wind-down (Jan 25 - Mar 11, 2024)
Unique Borrowers (Count) 237 NA NA NA NA NA NA
Period Issuance (Billions) 391 13.11 NA NA NA NA NA
Loan Amount (Millions) 391 13.11 33.54 7.50 151.83 0.00 2590.00
Interest Rate (%) 391 NA 5.40 5.40 0.00 5.40 5.40
Term (Days) 391 NA 294.72 365.00 141.09 1.00 368.00
Collateral Pledged (Millions) 391 69.38 177.44 32.80 530.79 0.02 5604.36
Utilization (%) 391 NA 39.21 29.41 34.04 0.00 100.00
Treasury Share (%) 391 NA 14.36 0.00 29.55 0.00 100.00
Note:
Baseline 1 excludes failed banks and GSIBs. P5-P6 have no DW comparison data.

2.3 Bank-Level Characteristics by Facility Choice

2.3.1 Summary Statistics by Facility (Full Program)

# ==========================================================================
# BANK CHARACTERISTICS BY FACILITY CHOICE - FULL PROGRAM
# ==========================================================================

cat("\n=== BANK CHARACTERISTICS BY FACILITY CHOICE ===\n")
#> 
#> === BANK CHARACTERISTICS BY FACILITY CHOICE ===
## Summary statistics for both baselines
create_facility_summary <- function(data, baseline_name) {
  data %>%
    group_by(facility) %>%
    summarize(
      N = n(),
      `Assets ($B)` = mean(assets, na.rm = TRUE) / 1e6,
      `MTM BTFP (%)` = mean(mtm_btfp, na.rm = TRUE) ,
      `MTM Other (%)` = mean(mtm_other, na.rm = TRUE) ,
      `Uninsured (%)` = mean(uninsured_lev, na.rm = TRUE),
      `Eligible Coll. (%)` = mean(eligible_collateral, na.rm = TRUE),
      `Adj. Equity (%)` = mean(adjusted_equity, na.rm = TRUE),
      `% MTM Insolvent` = mean(mtm_insolvent, na.rm = TRUE) ,
      .groups = "drop"
    )
}

## Baseline 1
summary_b1 <- create_facility_summary(df_1, "Baseline 1")
summary_b1 %>%
  kbl(caption = "Table 2.3A: Bank Characteristics by Facility Choice (Baseline 1, Full Program)",
      digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 2.3A: Bank Characteristics by Facility Choice (Baseline 1, Full Program)
facility N Assets ($B) MTM BTFP (%) MTM Other (%) Uninsured (%) Eligible Coll. (%) Adj. Equity (%) % MTM Insolvent
BTFP Only 846 2.53 0.82 4.99 24.42 11.29 2.29 29.67
DW Only 591 4.37 0.62 4.64 25.95 8.69 4.31 14.04
Both 413 8.84 0.87 4.95 28.69 10.84 2.62 23.24
Neither 2442 1.17 0.61 4.40 21.91 11.09 5.94 16.24
## Baseline 2
summary_b2 <- create_facility_summary(df_2, "Baseline 2")
summary_b2 %>%
  kbl(caption = "Table 2.3B: Bank Characteristics by Facility Choice (Baseline 2, Full Program)",
      digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 2.3B: Bank Characteristics by Facility Choice (Baseline 2, Full Program)
facility N Assets ($B) MTM BTFP (%) MTM Other (%) Uninsured (%) Eligible Coll. (%) Adj. Equity (%) % MTM Insolvent
BTFP Only 881 2.78 0.79 4.99 24.53 10.85 2.32 29.40
DW Only 647 10.01 0.58 4.62 25.77 8.08 4.54 14.22
Both 435 22.34 0.86 4.94 28.66 10.64 2.63 23.22
Neither 2774 1.79 0.55 4.44 21.53 9.86 7.00 15.02

2.3.2 Bank Characteristics with T-Statistics (Baseline 1)

# ==========================================================================
# BANK CHARACTERISTICS WITH T-TESTS VS "NEITHER" GROUP - BASELINE 1
# ==========================================================================

cat("\n=== T-TEST ANALYSIS (BASELINE 1) ===\n")
#> 
#> === T-TEST ANALYSIS (BASELINE 1) ===
## Helper function for t-tests
ttest_vs_neither <- function(data, var, group_var, group_val) {
  group_vals <- data[[var]][data[[group_var]] == group_val]
  neither_vals <- data[[var]][data[[group_var]] == "Neither"]
  
  group_vals <- group_vals[!is.na(group_vals)]
  neither_vals <- neither_vals[!is.na(neither_vals)]
  
  if (length(group_vals) < 2 | length(neither_vals) < 2) {
    return(list(mean = NA, sd = NA, t_stat = NA, p_val = NA, stars = ""))
  }
  
  test_result <- tryCatch(
    t.test(group_vals, neither_vals, var.equal = FALSE),
    error = function(e) NULL
  )
  
  if (is.null(test_result)) {
    return(list(mean = mean(group_vals, na.rm = TRUE), 
                sd = sd(group_vals, na.rm = TRUE), 
                t_stat = NA, p_val = NA, stars = ""))
  }
  
  p_val <- test_result$p.value
  
  return(list(
    mean = mean(group_vals, na.rm = TRUE),
    sd = sd(group_vals, na.rm = TRUE),
    t_stat = test_result$statistic,
    p_val = p_val,
    stars = add_stars(p_val)
  ))
}

## Variables to analyze
key_vars <- list(
  "MTM Loss BTFP-Elig. (%)" = "mtm_btfp",
  "MTM Loss Other (%)" = "mtm_other",
  "MTM Loss Total (%)" = "pct_mtm_loss",
  "Uninsured Deposits (%)" = "pct_uninsured",
  "BTFP-Eligible Coll. (%)" = "eligible_collateral",
  "Log(Assets)" = "ln_assets",
  "Cash Ratio (%)" = "cash_ratio",
  "Securities Ratio (%)" = "securities_ratio",
  "Book Equity Ratio (%)" = "book_equity_ratio",
  "Whole Sale Libility (%)" = "pct_wholesale_liability",
  "Loan-to-Deposit" = "loan_to_deposit",
  "ROA (%)" = "roa",
  "Adjusted Equity (%)" = "adjusted_equity",
  "Run Risk 1" = "run_risk_1",
  "Run Risk Dummy 1" = "run_risk_1_dummy",
  "MTM Insolvent (%)" = "mtm_insolvent"
  
)

## Build t-test table
results_b1 <- tibble()

for (var_label in names(key_vars)) {
  var_name <- key_vars[[var_label]]
  
  btfp_stats <- ttest_vs_neither(df_1, var_name, "facility", "BTFP Only")
  dw_stats <- ttest_vs_neither(df_1, var_name, "facility", "DW Only")
  both_stats <- ttest_vs_neither(df_1, var_name, "facility", "Both")
  neither_vals <- df_1[[var_name]][df_1$facility == "Neither"]
  neither_vals <- neither_vals[!is.na(neither_vals)]
  
  row <- tibble(
    Variable = var_label,
    `BTFP Only` = sprintf("%.3f%s", btfp_stats$mean, btfp_stats$stars),
    `DW Only` = sprintf("%.3f%s", dw_stats$mean, dw_stats$stars),
    `Both` = sprintf("%.3f%s", both_stats$mean, both_stats$stars),
    `Neither` = sprintf("%.3f", mean(neither_vals, na.rm = TRUE))
  )
  
  results_b1 <- bind_rows(results_b1, row)
}

results_b1 %>%
  kbl(caption = "Table 2.3C: Bank Characteristics by Facility with T-Statistics (Baseline 1)",
      escape = FALSE) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  footnote(general = "T-tests compare each group vs. Neither. * p<0.10, ** p<0.05, *** p<0.01",
           general_title = "Note:")
Table 2.3C: Bank Characteristics by Facility with T-Statistics (Baseline 1)
Variable BTFP Only DW Only Both Neither
MTM Loss BTFP-Elig. (%) 0.824*** 0.616 0.875*** 0.615
MTM Loss Other (%) 4.987*** 4.636*** 4.949*** 4.396
MTM Loss Total (%) 6.090*** 5.327 5.952*** 5.203
Uninsured Deposits (%) 28.159*** 30.373*** 33.722*** 26.021
BTFP-Eligible Coll. (%) 11.285 8.695*** 10.837 11.086
Log(Assets) 13.096*** 13.620*** 14.051*** 12.432
Cash Ratio (%) 5.685*** 7.257*** 4.864*** 9.772
Securities Ratio (%) 28.726*** 21.113*** 26.044 25.669
Book Equity Ratio (%) 8.380*** 9.639*** 8.576*** 11.277
Whole Sale Libility (%) 1.097* 1.026 1.410*** 0.891
Loan-to-Deposit 70.013 77.536*** 75.271*** 68.802
ROA (%) 1.007*** 1.217 1.067** 1.221
Adjusted Equity (%) 2.290*** 4.312*** 2.624*** 5.935
Run Risk 1 166.204*** 155.719*** 196.478*** 129.922
Run Risk Dummy 1 0.299*** 0.247*** 0.404*** 0.172
MTM Insolvent (%) 29.669*** 14.044 23.245*** 16.242
Note:
T-tests compare each group vs. Neither. * p<0.10, ** p<0.05, *** p<0.01

2.3.3 Bank Characteristics with T-Statistics (Baseline 2)

# ==========================================================================
# BANK CHARACTERISTICS WITH T-TESTS - BASELINE 2
# ==========================================================================

cat("\n=== T-TEST ANALYSIS (BASELINE 2) ===\n")
#> 
#> === T-TEST ANALYSIS (BASELINE 2) ===
## Build t-test table for Baseline 2
results_b2 <- tibble()

for (var_label in names(key_vars)) {
  var_name <- key_vars[[var_label]]
  
  btfp_stats <- ttest_vs_neither(df_2, var_name, "facility", "BTFP Only")
  dw_stats <- ttest_vs_neither(df_2, var_name, "facility", "DW Only")
  both_stats <- ttest_vs_neither(df_2, var_name, "facility", "Both")
  neither_vals <- df_2[[var_name]][df_2$facility == "Neither"]
  neither_vals <- neither_vals[!is.na(neither_vals)]
  
  row <- tibble(
    Variable = var_label,
    `BTFP Only` = sprintf("%.3f%s", btfp_stats$mean, btfp_stats$stars),
    `DW Only` = sprintf("%.3f%s", dw_stats$mean, dw_stats$stars),
    `Both` = sprintf("%.3f%s", both_stats$mean, both_stats$stars),
    `Neither` = sprintf("%.3f", mean(neither_vals, na.rm = TRUE))
  )
  
  results_b2 <- bind_rows(results_b2, row)
}

results_b2 %>%
  kbl(caption = "Table 2.3D: Bank Characteristics by Facility with T-Statistics (Baseline 2)",
      escape = FALSE) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) %>%
  footnote(general = "T-tests compare each group vs. Neither. * p<0.10, ** p<0.05, *** p<0.01",
           general_title = "Note:")
Table 2.3D: Bank Characteristics by Facility with T-Statistics (Baseline 2)
Variable BTFP Only DW Only Both Neither
MTM Loss BTFP-Elig. (%) 0.793*** 0.578 0.865*** 0.549
MTM Loss Other (%) 4.987*** 4.621 4.935*** 4.444
MTM Loss Total (%) 6.084*** 5.276 5.952*** 5.207
Uninsured Deposits (%) 28.290*** 30.286*** 33.710*** 26.196
BTFP-Eligible Coll. (%) 10.853** 8.080*** 10.643 9.858
Log(Assets) 13.080*** 13.612*** 14.137*** 12.386
Cash Ratio (%) 5.723*** 7.596*** 5.081*** 10.911
Securities Ratio (%) 28.476*** 20.529*** 26.030*** 23.902
Book Equity Ratio (%) 8.408*** 9.815*** 8.586*** 12.479
Whole Sale Libility (%) 1.097** 0.973 1.367*** 0.834
Loan-to-Deposit 70.261 78.462*** 74.768*** 69.632
ROA (%) 1.010* 1.204 1.076* 1.566
Adjusted Equity (%) 2.323*** 4.539*** 2.634*** 7.001
Run Risk 1 166.606*** 153.617** 195.320*** 134.049
Run Risk Dummy 1 0.314*** 0.246*** 0.402*** 0.168
MTM Insolvent (%) 29.398*** 14.219 23.218*** 15.022
Note:
T-tests compare each group vs. Neither. * p<0.10, ** p<0.05, *** p<0.01

2.4 Correlation Matrices

# ==========================================================================
# CORRELATION MATRICES
# ==========================================================================

cat("\n=== CORRELATION MATRICES ===\n")
#> 
#> === CORRELATION MATRICES ===
## Select key variables for correlation
corr_vars <- c("mtm_btfp", "mtm_other", "uninsured_lev", "eligible_collateral",
               "ln_assets", "cash_ratio", "securities_ratio", "book_equity_ratio",
               "loan_to_deposit", "pct_wholesale_liability", "roa", "adjusted_equity", "run_risk_1")

## Baseline 1
corr_matrix_b1 <- df_1 %>%
  select(all_of(corr_vars)) %>%
  cor(use = "pairwise.complete.obs")

corr_matrix_b1 %>%
  kbl(caption = "Table 2.4A: Correlation Matrix (Baseline 1)",
      digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE, font_size = 9)
Table 2.4A: Correlation Matrix (Baseline 1)
mtm_btfp mtm_other uninsured_lev eligible_collateral ln_assets cash_ratio securities_ratio book_equity_ratio loan_to_deposit pct_wholesale_liability roa adjusted_equity run_risk_1
mtm_btfp 1.000 -0.025 0.013 0.676 0.062 -0.074 0.424 -0.109 -0.308 0.040 -0.001 -0.169 0.218
mtm_other -0.025 1.000 -0.110 -0.303 0.109 -0.373 0.085 -0.203 0.114 -0.017 -0.083 -0.396 0.448
uninsured_lev 0.013 -0.110 1.000 -0.041 0.434 0.018 -0.037 -0.236 -0.025 -0.045 -0.066 -0.193 0.661
eligible_collateral 0.676 -0.303 -0.041 1.000 -0.132 0.078 0.534 0.079 -0.391 0.025 0.039 0.075 -0.048
ln_assets 0.062 0.109 0.434 -0.132 1.000 -0.267 -0.198 -0.188 0.265 0.060 -0.030 -0.176 0.363
cash_ratio -0.074 -0.373 0.018 0.078 -0.267 1.000 -0.107 0.286 -0.288 -0.044 0.066 0.348 -0.215
securities_ratio 0.424 0.085 -0.037 0.534 -0.198 -0.107 1.000 0.004 -0.669 0.068 0.076 -0.071 0.215
book_equity_ratio -0.109 -0.203 -0.236 0.079 -0.188 0.286 0.004 1.000 0.000 -0.026 0.443 0.973 -0.144
loan_to_deposit -0.308 0.114 -0.025 -0.391 0.265 -0.288 -0.669 0.000 1.000 0.048 -0.138 -0.053 -0.088
pct_wholesale_liability 0.040 -0.017 -0.045 0.025 0.060 -0.044 0.068 -0.026 0.048 1.000 0.040 -0.023 0.025
roa -0.001 -0.083 -0.066 0.039 -0.030 0.066 0.076 0.443 -0.138 0.040 1.000 0.470 0.142
adjusted_equity -0.169 -0.396 -0.193 0.075 -0.176 0.348 -0.071 0.973 -0.053 -0.023 0.470 1.000 -0.301
run_risk_1 0.218 0.448 0.661 -0.048 0.363 -0.215 0.215 -0.144 -0.088 0.025 0.142 -0.301 1.000
## Baseline 2
corr_matrix_b2 <- df_2 %>%
  select(all_of(corr_vars)) %>%
  cor(use = "pairwise.complete.obs")

corr_matrix_b2 %>%
  kbl(caption = "Table 2.4B: Correlation Matrix (Baseline 2)",
      digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE, font_size = 9)
Table 2.4B: Correlation Matrix (Baseline 2)
mtm_btfp mtm_other uninsured_lev eligible_collateral ln_assets cash_ratio securities_ratio book_equity_ratio loan_to_deposit pct_wholesale_liability roa adjusted_equity run_risk_1
mtm_btfp 1.000 -0.015 0.042 0.696 0.111 -0.100 0.436 -0.128 -0.296 0.047 -0.015 -0.167 0.068
mtm_other -0.015 1.000 -0.083 -0.159 0.030 -0.149 0.042 -0.120 0.050 -0.014 -0.036 -0.459 0.865
uninsured_lev 0.042 -0.083 1.000 -0.007 0.436 -0.014 -0.010 -0.269 -0.022 -0.032 -0.029 -0.211 0.172
eligible_collateral 0.696 -0.159 -0.007 1.000 -0.060 -0.001 0.541 0.001 -0.371 0.036 -0.009 0.008 -0.015
ln_assets 0.111 0.030 0.436 -0.060 1.000 -0.244 -0.131 -0.214 0.223 0.062 -0.018 -0.178 0.089
cash_ratio -0.100 -0.149 -0.014 -0.001 -0.244 1.000 -0.177 0.421 -0.321 -0.057 0.092 0.415 0.015
securities_ratio 0.436 0.042 -0.010 0.541 -0.131 -0.177 1.000 -0.059 -0.633 0.073 -0.008 -0.110 0.054
book_equity_ratio -0.128 -0.120 -0.269 0.001 -0.214 0.421 -0.059 1.000 -0.104 -0.039 0.133 0.932 -0.008
loan_to_deposit -0.296 0.050 -0.022 -0.371 0.223 -0.321 -0.633 -0.104 1.000 0.055 -0.073 -0.128 -0.070
pct_wholesale_liability 0.047 -0.014 -0.032 0.036 0.062 -0.057 0.073 -0.039 0.055 1.000 0.008 -0.032 0.005
roa -0.015 -0.036 -0.029 -0.009 -0.018 0.092 -0.008 0.133 -0.073 0.008 1.000 0.146 -0.003
adjusted_equity -0.167 -0.459 -0.211 0.008 -0.178 0.415 -0.110 0.932 -0.128 -0.032 0.146 1.000 -0.453
run_risk_1 0.068 0.865 0.172 -0.015 0.089 0.015 0.054 -0.008 -0.070 0.005 -0.003 -0.453 1.000

2.5 Balance Sheet Composition (2022Q4)

# ==========================================================================
# BALANCE SHEET COMPOSITION - 2022Q4
# ==========================================================================

cat("\n=== BALANCE SHEET COMPOSITION (2022Q4) ===\n")
#> 
#> === BALANCE SHEET COMPOSITION (2022Q4) ===
## Asset composition
asset_composition_b1 <- df_1 %>%
  summarise(
    `Cash (%)` = mean(cash_ratio, na.rm = TRUE),
    `Securities (%)` = mean(securities_ratio, na.rm = TRUE),
    `BTFP-Eligible (%)` = mean(eligible_collateral, na.rm = TRUE),
    `Loans (%)` = mean(total_loan / assets, na.rm = TRUE) * 100,
    `Other Assets (%)` = 100 - (`Cash (%)` + `Securities (%)` + `Loans (%)`)
  )

asset_composition_b1 %>%
  pivot_longer(everything(), names_to = "Category", values_to = "Percent") %>%
  kbl(caption = "Table 2.5A: Average Asset Composition 2022Q4 (Baseline 1)",
      digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 2.5A: Average Asset Composition 2022Q4 (Baseline 1)
Category Percent
Cash (%) 8.15
Securities (%) 25.68
BTFP-Eligible (%) 10.77
Loans (%) 59.99
Other Assets (%) 6.18
## Liability composition
liab_composition_b1 <- df_1 %>%
  summarise(
    `Total Deposits (%)` = mean(total_deposit / assets, na.rm = TRUE) * 100,
    `  Insured (%)` = mean((insured_deposit / assets), na.rm = TRUE) * 100,
    `  Uninsured (%)` = mean(uninsured_lev, na.rm = TRUE),
    `Wholesale Funding (%)` = mean(pct_wholesale_liability, na.rm = TRUE),
    `FHLB Advances (%)` = mean(fhlb_ratio, na.rm = TRUE),
    `Equity (%)` = mean(book_equity_ratio, na.rm = TRUE)
  )

liab_composition_b1 %>%
  pivot_longer(everything(), names_to = "Category", values_to = "Percent") %>%
  kbl(caption = "Table 2.5B: Average Liability Composition 2022Q4 (Baseline 1)",
      digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 2.5B: Average Liability Composition 2022Q4 (Baseline 1)
Category Percent
Total Deposits (%) 85.23
Insured (%) 61.75
Uninsured (%) 23.61
Wholesale Funding (%) 1.00
FHLB Advances (%) 2.65
Equity (%) 10.22
cat("\n")
cat(strrep("=", 80), "\n")
#> ================================================================================
cat("✓✓✓ PART II COMPLETE: Descriptive Statistics and Summary Tables ✓✓✓\n")
#> ✓✓✓ PART II COMPLETE: Descriptive Statistics and Summary Tables ✓✓✓
cat(strrep("=", 80), "\n")
#> ================================================================================

Descriptive Statistics: Asset and Liability Composition

################################################################################
# ASSET AND LIABILITY COMPOSITION TABLES
################################################################################

cat("\n")
cat(strrep("=", 80), "\n", sep = "")
#> ================================================================================
cat("ASSET AND LIABILITY COMPOSITION TABLES\n")
#> ASSET AND LIABILITY COMPOSITION TABLES
cat(strrep("=", 80), "\n", sep = "")
#> ================================================================================
cat("\nGenerating descriptive tables for 2022Q4 and 2023Q1\n\n")
#> 
#> Generating descriptive tables for 2022Q4 and 2023Q1
# Helper function for winsorization at 5-95 percentiles
winsorize_5_95 <- function(x) {
  if (all(is.na(x))) return(x)
  q <- quantile(x, probs = c(0.05, 0.95), na.rm = TRUE, names = FALSE)
  # Clip values outside the 5th-95th range
  pmax(pmin(x, q[2]), q[1])
}

# Helper to safely sum columns (treating NA as 0)
safe_sum <- function(...) {
  rowSums(cbind(...), na.rm = TRUE)
}
# Asset Configuration
ASSET_CONFIG <- list(
  list("Cash", "cash"),
  list("Securities", "security"),
  list("\\quad Treasury", "treasury"),
  list("\\quad RMBS", "total_rmbs"),
  list("\\quad CMBS", "total_cmbs"),
  list("\\quad ABS", "abs"),
  list("\\quad Other Securities", "other_security"), # Residual
  list("Total Loans", "total_loan"),
  list("\\quad Real Estate Loans", "reloan"),
  list("\\quad\\quad Residential Mortgage", "reloan_residential1to4"),
  list("\\quad\\quad Commercial Mortgage", "reloan_residential5"),
  list("\\quad\\quad Other Real Estate", "other_re_residual"), # Residual
  list("\\quad Agricultural Loans", "agloan"),
  list("\\quad Commercial & Industrial", "ciloan"),
  list("\\quad Consumer Loans", "hhloan"),
  list("\\quad Loans to Non-Depositories", "loans_to_nonbank"),
  list("Fed Funds Sold", "fed_fund_sold"),
  list("Reverse Repo", "rerepo")
)

# Liability Configuration
LIAB_CONFIG <- list(
  list("Total Liabilities", "r_total_liability"),
  list("Domestic Deposits", "r_dom_deposit"),
  list("\\quad Insured Deposits", "r_insured_deposit"),
  list("\\quad Uninsured Deposits", "r_uninsured_deposit"),
  list("\\quad\\quad Uninsured Time Deposits (Total)", "r_uninsured_tot_time_deposit"),
  list("\\quad\\quad\\quad Long-term Time Deposits", "r_uninsured_long_time_account"),
  list("\\quad\\quad\\quad Short-term Time Deposits", "r_uninsured_short_time_deposit"),
  list("Foreign Deposits", "r_foreign_deposit"),
  list("Fed Funds Purchased", "r_fed_fund_purchase"),
  list("Repo", "r_repo"),
  list("Other Liabilities", "r_other_liab"),
  list("Total Equity", "r_total_equity"),
  list("\\quad Common Stock", "r_common_stock"),
  list("\\quad Preferred Stock", "r_preferred_stock"),
  list("\\quad Retained Earnings", "r_retained_earning")
)
# Function to prepare data (calculate residuals and ensure grouping)
prepare_data <- function(df) {
  
  # --- 1. Pre-calculation helpers ---
  # Get column or 0 if missing
  get_col <- function(var) if(var %in% names(df)) df[[var]] else 0
  
  # --- 2. Create Raw Components needed for Residuals ---
  # These sums match the "Strict Jiang" definition in the Python notebook
  # ABS = HTM Amortized + HFS Fair
  abs_val <- safe_sum(get_col("abs_htm_amortize"), get_col("abs_hfs_fair"))
  
  # RMBS = Agency (HTM+HFS) + Other (HTM+HFS)
  rmbs_val <- safe_sum(get_col("agency_rmbs_htm_amortize"), get_col("agency_rmbs_hfs_fair"),
                       get_col("other_rmbs_htm_amortize"), get_col("other_rmbs_hfs_fair"))
  
  # CMBS = Agency (HTM+HFS) + Other (HTM+HFS)
  cmbs_val <- safe_sum(get_col("agency_cmbs_htm_amortize"), get_col("agency_cmbs_hfs_fair"),
                       get_col("other_cmbs_htm_amortize"), get_col("other_cmbs_hfs_fair"))

  df %>%
    mutate(
      # --- Categories ---
      bank_category = case_when(
        size_bin == "small" ~ "Small",
        size_bin == "large" ~ "Large",
        size_bin == "gsib" ~ "GSIB",
        TRUE ~ NA_character_
      ),
      
      # --- Assign Constructed Variables ---
      abs = abs_val,
      total_rmbs = rmbs_val,
      total_cmbs = cmbs_val,
      
      # --- Calculate Asset Residuals ---
      # Other Securities = Security - (Treasury + RMBS + CMBS + ABS)
      other_security = security - safe_sum(treasury, rmbs_val, cmbs_val, abs_val),
      
      # Other Real Estate = Reloan - (Res + Comm)
      other_re_residual = reloan - safe_sum(reloan_residential1to4, reloan_residential5),
      
      # --- Calculate Missing Liability Ratios (r_*) ---
      # If these columns are missing, calculate them as: 100 * Raw / Total Assets
      r_uninsured_long_time_account = if("r_uninsured_long_time_account" %in% names(.)) 
          r_uninsured_long_time_account 
        else 
          100 * get_col("uninsured_long_time_account") / total_asset,
          
      r_uninsured_short_time_deposit = if("r_uninsured_short_time_deposit" %in% names(.)) 
          r_uninsured_short_time_deposit 
        else 
          100 * get_col("uninsured_short_time_deposit") / total_asset,
          
      r_uninsured_tot_time_deposit = if("r_uninsured_tot_time_deposit" %in% names(.)) 
          r_uninsured_tot_time_deposit 
        else 
          100 * safe_sum(get_col("uninsured_long_time_account"), get_col("uninsured_short_time_deposit")) / total_asset
    )
}

# Apply preparation
df_2022Q4 <- prepare_data(call_q %>% filter(quarter == "2022Q4"))
df_2023Q1 <- prepare_data(call_q %>% filter(quarter == "2023Q1"))

cat("✓ Data Prepared successfully.\n")
#> ✓ Data Prepared successfully.
generate_jiang_table <- function(df, config, type = "asset") {
  
  # --- 1. Calculate N (Counts) ---
  n_total <- nrow(df)
  n_small <- sum(df$bank_category == "Small", na.rm = TRUE)
  n_large <- sum(df$bank_category == "Large", na.rm = TRUE)
  n_gsib  <- sum(df$bank_category == "GSIB",  na.rm = TRUE)
  
  # Initialize DataFrame with N row
  stats_df <- data.frame(
    Component = "Number of Banks",
    Agg = format(n_total, big.mark=","), 
    Full = format(n_total, big.mark=","),
    Small = format(n_small, big.mark=","),
    Large = format(n_large, big.mark=","),
    GSIB = format(n_gsib, big.mark=","),
    stringsAsFactors = FALSE
  )
  
  total_asset_sum <- sum(df$total_asset, na.rm = TRUE)
  
  # --- 2. Process Components ---
  for (item in config) {
    label <- item[[1]]
    var_name <- item[[2]]
    
    # Get Data
    if (!var_name %in% names(df)) {
      vals <- rep(0, nrow(df))
    } else {
      vals <- df[[var_name]]
    }
    
    # Standardize to Percentage
    if (type == "asset") {
      bank_ratios <- 100 * vals / df$total_asset
      agg_val <- 100 * sum(vals, na.rm = TRUE) / total_asset_sum
    } else {
      bank_ratios <- vals
      agg_val <- sum(vals * df$total_asset, na.rm = TRUE) / total_asset_sum
    }
    
    # GLOBAL WINSORIZATION
    winsorized_ratios <- winsorize_5_95(bank_ratios)
    
    # Calculate Stats on Winsorized Vector
    calc_stat <- function(data_vec) {
      if (length(data_vec) == 0) return(c(0, 0))
      return(c(mean(data_vec, na.rm = TRUE), sd(data_vec, na.rm = TRUE)))
    }
    
    full_stats  <- calc_stat(winsorized_ratios)
    small_stats <- calc_stat(winsorized_ratios[df$bank_category == "Small"])
    large_stats <- calc_stat(winsorized_ratios[df$bank_category == "Large"])
    gsib_stats  <- calc_stat(winsorized_ratios[df$bank_category == "GSIB"])
    
    # Format Row
    fmt <- function(s) sprintf("%.1f\n(%.1f)", s[1], s[2])
    
    stats_df[nrow(stats_df) + 1, ] <- list(
      Component = label,
      Agg = sprintf("%.1f", agg_val),
      Full = fmt(full_stats),
      Small = fmt(small_stats),
      Large = fmt(large_stats),
      GSIB = fmt(gsib_stats)
    )
  }
  
  return(stats_df)
}

render_table <- function(stats_df, title) {
  stats_df %>%
    kbl(caption = title,
        col.names = c("", "Aggregate", "Full Sample", "Small", "Large", "GSIB"),
        align = c("l", "c", "c", "c", "c", "c"),
        escape = FALSE) %>%
    kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = F) %>%
    add_header_above(c(" " = 1, "(1)" = 1, "(2)" = 1, "(3)" = 1, "(4)" = 1, "(5)" = 1)) %>%
    row_spec(0, bold = TRUE) %>%
    # Add a line after the N row (row 1)
    row_spec(1, hline_after = TRUE) %>%
    footnote(general = "Values are percentages of total assets. Row 1 shows number of banks. Column (1) is aggregate. Columns (2)-(5) are bank-level averages. Standard deviations in parentheses. All bank-level statistics (Cols 2-5) are winsorized at the 5th and 95th percentiles.")
}
# --- 2022Q4 ---
asset_stats_22q4 <- generate_jiang_table(df_2022Q4, ASSET_CONFIG, type = "asset")
render_table(asset_stats_22q4, "Bank Asset Composition, 2022Q4")
Bank Asset Composition, 2022Q4
(1)
(2)
(3)
(4)
(5)
Aggregate Full Sample Small Large GSIB
Number of Banks 4,737 4,737 3,967 737 33
Cash 11.0 8.1 (7.4) 8.5 (7.5) 5.2 (5.6) 18.7 (10.1)
Securities 24.0 24.0 (14.9) 24.7 (15.2) 20.5 (12.2) 17.4 (17.4)
Treasury 6.0 3.3 (4.8) 3.5 (5.0) 2.3 (3.7) 5.1 (6.1)
RMBS 11.2 3.0 (4.3) 2.4 (3.9) 6.1 (5.1) 4.9 (6.1)
CMBS 1.8 0.8 (1.4) 0.6 (1.3) 1.3 (1.6) 0.8 (1.4)
ABS 3.1 1.0 (1.7) 0.9 (1.6) 1.5 (1.9) 1.4 (2.3)
Other Securities 1.9 14.5 (12.2) 15.9 (12.4) 7.4 (7.7) 2.3 (5.9)
Total Loans 51.0 60.8 (16.5) 59.7 (16.6) 67.4 (13.6) 43.6 (18.9)
Real Estate Loans 24.2 45.9 (18.3) 45.4 (18.2) 49.7 (17.4) 22.0 (18.6)
Residential Mortgage 11.7 17.3 (12.8) 17.7 (13.0) 15.6 (11.7) 11.8 (15.4)
Commercial Mortgage 2.5 2.3 (2.8) 2.0 (2.6) 4.0 (3.1) 1.1 (2.1)
Other Real Estate 10.0 24.8 (12.9) 24.5 (12.7) 27.9 (12.8) 4.3 (6.2)
Agricultural Loans 0.3 3.0 (4.8) 3.4 (5.1) 0.8 (2.1) 0.1 (0.4)
Commercial & Industrial 9.8 7.2 (5.4) 6.8 (5.1) 9.4 (6.4) 4.4 (5.9)
Consumer Loans 8.6 2.4 (2.7) 2.3 (2.5) 2.5 (3.4) 3.5 (4.3)
Loans to Non-Depositories 3.2 0.1 (0.2) 0.0 (0.2) 0.2 (0.3) 0.4 (0.4)
Fed Funds Sold 0.1 0.8 (1.8) 0.9 (1.9) 0.1 (0.6) 0.6 (1.9)
Reverse Repo 1.3 0.0 (0.0) 0.0 (0.0) 0.0 (0.0) 0.0 (0.0)
Note:
Values are percentages of total assets. Row 1 shows number of banks. Column (1) is aggregate. Columns (2)-(5) are bank-level averages. Standard deviations in parentheses. All bank-level statistics (Cols 2-5) are winsorized at the 5th and 95th percentiles.
liab_stats_22q4 <- generate_jiang_table(df_2022Q4, LIAB_CONFIG, type = "liability")
render_table(liab_stats_22q4, "Bank Liability Composition, 2022Q4")
Bank Liability Composition, 2022Q4
(1)
(2)
(3)
(4)
(5)
Aggregate Full Sample Small Large GSIB
Number of Banks 4,737 4,737 3,967 737 33
Total Liabilities 90.6 90.3 (3.7) 90.3 (3.8) 90.4 (2.9) 86.9 (4.7)
Domestic Deposits 75.1 85.7 (6.4) 86.1 (6.4) 83.6 (5.7) 77.0 (7.9)
Insured Deposits 42.1 61.8 (12.7) 63.6 (11.9) 52.8 (12.0) 46.9 (18.4)
Uninsured Deposits 35.8 23.0 (11.0) 21.7 (10.4) 30.2 (11.0) 23.1 (17.0)
Uninsured Time Deposits (Total) 2.2 4.1 (3.4) 4.3 (3.4) 3.4 (3.0) 1.5 (2.4)
Long-term Time Deposits 0.3 1.1 (1.2) 1.1 (1.2) 0.8 (0.9) 0.1 (0.3)
Short-term Time Deposits 1.9 2.9 (2.6) 3.0 (2.6) 2.6 (2.4) 1.3 (2.2)
Foreign Deposits 6.3 0.0 (0.0) 0.0 (0.0) 0.0 (0.0) 0.0 (0.0)
Fed Funds Purchased 0.2 0.2 (0.5) 0.2 (0.5) 0.1 (0.4) 0.0 (0.0)
Repo 0.6 0.2 (0.7) 0.2 (0.6) 0.5 (0.9) 0.2 (0.6)
Other Liabilities 7.2 3.6 (4.0) 3.2 (3.9) 5.5 (4.1) 6.2 (4.4)
Total Equity 9.4 9.7 (3.7) 9.7 (3.8) 9.6 (2.9) 13.1 (4.7)
Common Stock 0.2 0.4 (0.6) 0.4 (0.6) 0.2 (0.6) 0.7 (1.0)
Preferred Stock 0.1 0.0 (0.0) 0.0 (0.0) 0.0 (0.0) 0.0 (0.0)
Retained Earnings 4.4 7.2 (4.2) 7.4 (4.3) 6.1 (3.2) 7.4 (5.3)
Note:
Values are percentages of total assets. Row 1 shows number of banks. Column (1) is aggregate. Columns (2)-(5) are bank-level averages. Standard deviations in parentheses. All bank-level statistics (Cols 2-5) are winsorized at the 5th and 95th percentiles.

PART III: REGRESSION ANALYSIS

This section contains all regression analyses organized by: - Section 3.1: Step 1A - Extensive Margin (Baseline 1) - Section 3.2: Step 1B - Extensive Margin (Baseline 2) - Section 3.3: Step 2A - Temporal Analysis by OLD Periods (Acute, Post-Acute, Arbitrage) - Section 3.4: Step 2B - Temporal Analysis by NEW Periods (P1-P6) - Section 3.5: Step 3A/3B - Intensive Margin Analysis - Section 3.6: Step 4A/4B - Both Banks / Collateral Constraints

3.1 Step 1A: Extensive Margin (Baseline 1)

3.1.1 Main Specification - BTFP vs DW (Full Program Period)

# ==========================================================================
# STEP 1A: EXTENSIVE MARGIN - BASELINE 1
# Full program period analysis for BTFP and DW usage
# ==========================================================================

cat("\n=== STEP 1A: EXTENSIVE MARGIN (BASELINE 1) ===\n")
#> 
#> === STEP 1A: EXTENSIVE MARGIN (BASELINE 1) ===
cat("Sample:", nrow(df_1), "banks (excludes failed, GSIBs; requires OMO-eligible>0)\n\n")
#> Sample: 4292 banks (excludes failed, GSIBs; requires OMO-eligible>0)
## Define control variables
controls <- "ln_assets + cash_ratio + securities_ratio + loan_to_deposit + book_equity_ratio + pct_wholesale_liability + fhlb_ratio + roa"

## Main specification: MTM losses + interaction with uninsured deposits
base_rhs <- paste(
  "pct_mtm_loss + uninsured_lev + I(pct_mtm_loss * uninsured_lev)",
  controls,
  sep = " + "
)

## Estimate LPM for BTFP usage (full program)
lpm_btfp_b1 <- feols(
  as.formula(paste("btfp ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## Estimate LPM for DW usage (post-BTFP launch)
lpm_dw_b1 <- feols(
  as.formula(paste("dw ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## Display results
modelsummary(
  list("BTFP (Full)" = lpm_btfp_b1, "DW (Post-Launch)" = lpm_dw_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = c(
    
    "pct_mtm_lossr" = "MTM Loss (%)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(pct_mtm_loss * uninsured_lev)" = "MTM_Loss × Uninsured",
    "ln_assets" = "Log(Assets)",
    "cash_ratio" = "Cash Ratio",
    "securities_ratio" = "Securities Ratio",
    "loan_to_deposit" = "Loan-to-Deposit",
    "book_equity_ratio" = "Book Equity Ratio",
    "pct_wholesale_liability" = "% Wholesale Liab.",
    "fhlb_ratio" = "FHLB Ratio",
    "roa" = "ROA"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.1A: Extensive Margin - BTFP vs DW (Baseline 1)",
  notes = "Linear probability models with heteroskedasticity-robust standard errors. Baseline 1 excludes failed banks and GSIBs."
)
Table 3.1A: Extensive Margin - BTFP vs DW (Baseline 1)
BTFP (Full) DW (Post-Launch)
* p < 0.1, ** p < 0.05, *** p < 0.01
Linear probability models with heteroskedasticity-robust standard errors. Baseline 1 excludes failed banks and GSIBs.
Uninsured Deposits (%) -0.004*** -0.003**
(0.001) (0.001)
MTM_Loss × Uninsured 0.001*** 0.001***
(0.000) (0.000)
Log(Assets) 0.055*** 0.089***
(0.006) (0.006)
Cash Ratio -0.003*** -0.001
(0.001) (0.001)
Securities Ratio 0.003*** -0.001*
(0.001) (0.001)
Loan-to-Deposit 0.000 -0.000
(0.000) (0.000)
Book Equity Ratio -0.002*** 0.000
(0.001) (0.001)
% Wholesale Liab. 0.003 0.002
(0.002) (0.002)
FHLB Ratio 0.007*** 0.002
(0.002) (0.002)
ROA -0.001 0.001
(0.001) (0.002)
N 4282 4282
0.103 0.121

3.1.2 Run Risk Specifications

# ==========================================================================
# RUN RISK SPECIFICATIONS - BASELINE 1
# ==========================================================================

cat("\n=== RUN RISK SPECIFICATIONS (BASELINE 1) ===\n")
#> 
#> === RUN RISK SPECIFICATIONS (BASELINE 1) ===
## Continuous run risk measure
rhs_run_cont <- paste(base_rhs, "run_risk_1", sep = " + ")
lpm_btfp_run_cont <- feols(
  as.formula(paste("btfp ~", rhs_run_cont)),
  data = df_1,
  vcov = "hetero"
)

## Dummy for high run risk
rhs_run_dummy <- paste(base_rhs, "run_risk_1_dummy", sep = " + ")
lpm_btfp_run_dummy <- feols(
  as.formula(paste("btfp ~", rhs_run_dummy)),
  data = df_1,
  vcov = "hetero"
)

modelsummary(
  list("Main" = lpm_btfp_b1, 
       "Run Risk (Cont)" = lpm_btfp_run_cont, 
       "Run Risk (Dummy)" = lpm_btfp_run_dummy),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "pct_mtm_lossr" = "MTM Loss (%)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(pct_mtm_loss * uninsured_lev)" = "MTM_Loss × Uninsured",
    "run_risk_1" = "Run Risk (Continuous)",
    "run_risk_1_dummy" = "Run Risk (Dummy)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.1B: Run Risk Specifications (Baseline 1)"
)
Table 3.1B: Run Risk Specifications (Baseline 1)
Main Run Risk (Cont) Run Risk (Dummy)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -0.534*** -0.497***
(0.079) (0.078) (0.079)
pct_mtm_loss -0.005 -0.002 -0.005
(0.005) (0.005) (0.005)
Uninsured Deposits (%) -0.004*** -0.003** -0.004***
(0.001) (0.001) (0.001)
MTM_Loss × Uninsured 0.001*** 0.001*** 0.001***
(0.000) (0.000) (0.000)
ln_assets 0.055*** 0.055*** 0.055***
(0.006) (0.006) (0.006)
cash_ratio -0.003*** -0.003*** -0.003***
(0.001) (0.001) (0.001)
securities_ratio 0.003*** 0.003*** 0.003***
(0.001) (0.001) (0.001)
loan_to_deposit 0.000 0.001 0.000
(0.000) (0.000) (0.000)
book_equity_ratio -0.002*** -0.004*** -0.002***
(0.001) (0.001) (0.001)
pct_wholesale_liability 0.003 0.003 0.003
(0.002) (0.002) (0.002)
fhlb_ratio 0.007*** 0.007*** 0.007***
(0.002) (0.002) (0.002)
roa -0.001 -0.001 -0.001
(0.001) (0.003) (0.002)
Run Risk (Continuous) -0.000
(0.000)
Run Risk (Dummy) 0.000
(0.024)
N 4282 4251 4279
0.103 0.102 0.102

3.1.3 Insolvency Specifications (Jiang Methodology)

# ==========================================================================
# INSOLVENCY SPECIFICATIONS - BASELINE 1
# Following Jiang et al. methodology
# ==========================================================================

cat("\n=== INSOLVENCY SPECIFICATIONS (BASELINE 1) ===\n")
#> 
#> === INSOLVENCY SPECIFICATIONS (BASELINE 1) ===
## Model 1: Adjusted Equity (Book Equity - MTM Losses)
rhs_adj_equity <- paste("adjusted_equity", controls, sep = " + ")
lpm_adj_equity <- feols(
  as.formula(paste("btfp ~", rhs_adj_equity)),
  data = df_1,
  vcov = "hetero"
)

## Model 2: IDCR (Insured Deposit Coverage Ratio, s=1.0)
rhs_idcr <- paste("idcr_2", controls, sep = " + ")
lpm_idcr <- feols(
  as.formula(paste("btfp ~", rhs_idcr)),
  data = df_1,
  vcov = "hetero"
)

## Model 3: Capital Insolvency Indicator (s=1.0)
rhs_insolvency <- paste("insolvency_2", controls, sep = " + ")
lpm_insolvency <- feols(
  as.formula(paste("btfp ~", rhs_insolvency)),
  data = df_1,
  vcov = "hetero"
)

modelsummary(
  list("Adjusted Equity" = lpm_adj_equity,
       "IDCR (s=1.0)" = lpm_idcr,
       "Insolvent (s=1.0)" = lpm_insolvency),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  
  # CHANGE: Use coef_rename to rename specific vars but KEEP the others
  coef_rename = c(
    "adjusted_equity" = "Adjusted Equity (%)",
    "idcr_2" = "IDCR (s=1.0)",
    "insolvency_2" = "Insolvent (s=1.0)"
  ),
  
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.1C: Insolvency Specifications (Baseline 1)"
)
Table 3.1C: Insolvency Specifications (Baseline 1)
Adjusted Equity IDCR (s=1.0) Insolvent (s=1.0)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.665*** -0.583*** -0.580***
(0.079) (0.077) (0.077)
Adjusted Equity (%) -0.017***
(0.003)
ln_assets 0.064*** 0.063*** 0.063***
(0.005) (0.005) (0.005)
cash_ratio -0.003*** -0.004*** -0.004***
(0.001) (0.001) (0.001)
securities_ratio 0.003*** 0.004*** 0.004***
(0.001) (0.001) (0.001)
loan_to_deposit -0.000 0.000 0.000
(0.000) (0.000) (0.000)
book_equity_ratio 0.014*** -0.003*** -0.002*
(0.003) (0.001) (0.001)
pct_wholesale_liability 0.003 0.002 0.002
(0.002) (0.002) (0.002)
fhlb_ratio 0.006*** 0.007*** 0.007***
(0.002) (0.002) (0.002)
roa -0.000 -0.000 -0.000
(0.001) (0.001) (0.001)
IDCR (s=1.0) -0.000
(0.000)
Insolvent (s=1.0) -0.080
(0.114)
N 4282 4282 4282
0.096 0.091 0.091

3.2 Step 1B: Extensive Margin (Baseline 2)

3.2.1 Main Specification - BTFP vs DW (Full Program, All Banks)

# ==========================================================================
# STEP 1B: EXTENSIVE MARGIN - BASELINE 2 (ALL BANKS)
# ==========================================================================

cat("\n=== STEP 1B: EXTENSIVE MARGIN (BASELINE 2) ===\n")
#> 
#> === STEP 1B: EXTENSIVE MARGIN (BASELINE 2) ===
cat("Sample:", nrow(df_2), "banks (includes all banks with valid data)\n\n")
#> Sample: 4737 banks (includes all banks with valid data)
## Same specification as Baseline 1
lpm_btfp_b2 <- feols(
  as.formula(paste("btfp ~", base_rhs)),
  data = df_2,
  vcov = "hetero"
)

lpm_dw_b2 <- feols(
  as.formula(paste("dw ~", base_rhs)),
  data = df_2,
  vcov = "hetero"
)

modelsummary(
  list("BTFP (Full) B2" = lpm_btfp_b2, "DW (Post-Launch) B2" = lpm_dw_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "pct_mtm_lossr" = "MTM Loss (%)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(pct_mtm_loss * uninsured_lev)" = "MTM_Loss × Uninsured",
    "ln_assets" = "Log(Assets)",
    "cash_ratio" = "Cash Ratio",
    "securities_ratio" = "Securities Ratio",
    "loan_to_deposit" = "Loan-to-Deposit",
    "book_equity_ratio" = "Book Equity Ratio",
    "pct_wholesale_liability" = "% Wholesale Liab.",
    "fhlb_ratio" = "FHLB Ratio",
    "roa" = "ROA"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.2A: Extensive Margin - BTFP vs DW (Baseline 2, All Banks)",
  notes = "Linear probability models with heteroskedasticity-robust standard errors. Baseline 2 includes all banks."
)
Table 3.2A: Extensive Margin - BTFP vs DW (Baseline 2, All Banks)
BTFP (Full) B2 DW (Post-Launch) B2
* p < 0.1, ** p < 0.05, *** p < 0.01
Linear probability models with heteroskedasticity-robust standard errors. Baseline 2 includes all banks.
(Intercept) -0.482*** -0.904***
(0.070) (0.074)
pct_mtm_loss -0.002*** -0.002
(0.001) (0.001)
Uninsured Deposits (%) -0.003*** -0.001
(0.001) (0.001)
MTM_Loss × Uninsured 0.001*** 0.000***
(0.000) (0.000)
Log(Assets) 0.054*** 0.086***
(0.005) (0.005)
Cash Ratio -0.003*** -0.001
(0.001) (0.001)
Securities Ratio 0.002*** -0.001
(0.001) (0.001)
Loan-to-Deposit -0.000 0.000
(0.000) (0.000)
Book Equity Ratio -0.002*** 0.000
(0.000) (0.000)
% Wholesale Liab. 0.004* 0.002
(0.002) (0.002)
FHLB Ratio 0.005*** 0.002
(0.002) (0.002)
ROA 0.000 -0.000
(0.000) (0.000)
N 4718 4718
0.107 0.123

3.2.2 Run Risk and Insolvency (Baseline 2)

# ==========================================================================
# RUN RISK & INSOLVENCY - BASELINE 2
# ==========================================================================

cat("\n=== RUN RISK & INSOLVENCY (BASELINE 2) ===\n")
#> 
#> === RUN RISK & INSOLVENCY (BASELINE 2) ===
## Run risk dummy
lpm_btfp_run_b2 <- feols(
  as.formula(paste("btfp ~", rhs_run_dummy)),
  data = df_2,
  vcov = "hetero"
)

## Adjusted equity
lpm_adj_equity_b2 <- feols(
  as.formula(paste("btfp ~", rhs_adj_equity)),
  data = df_2,
  vcov = "hetero"
)

## Insolvency indicator
lpm_insolvency_b2 <- feols(
  as.formula(paste("btfp ~", rhs_insolvency)),
  data = df_2,
  vcov = "hetero"
)

modelsummary(
  list("Main (B2)" = lpm_btfp_b2,
       "Run Risk (B2)" = lpm_btfp_run_b2,
       "Adj. Equity (B2)" = lpm_adj_equity_b2,
       "Insolvent (B2)" = lpm_insolvency_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "pct_mtm_lossr" = "MTM Loss (%)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(pct_mtm_loss * uninsured_lev)" = "MTM_Loss × Uninsured",
    "run_risk_1_dummy" = "Run Risk (Dummy)",
    "adjusted_equity" = "Adjusted Equity (%)",
    "insolvency_2" = "Insolvent (s=1.0)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.2B: Alternative Specifications (Baseline 2)"
)
Table 3.2B: Alternative Specifications (Baseline 2)
Main (B2) Run Risk (B2) Adj. Equity (B2) Insolvent (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.482*** -0.481*** -0.566*** -0.535***
(0.070) (0.070) (0.075) (0.074)
pct_mtm_loss -0.002*** -0.001***
(0.001) (0.001)
Uninsured Deposits (%) -0.003*** -0.003***
(0.001) (0.001)
MTM_Loss × Uninsured 0.001*** 0.001***
(0.000) (0.000)
ln_assets 0.054*** 0.054*** 0.062*** 0.061***
(0.005) (0.005) (0.004) (0.004)
cash_ratio -0.003*** -0.003*** -0.003*** -0.004***
(0.001) (0.001) (0.001) (0.001)
securities_ratio 0.002*** 0.002*** 0.003*** 0.003***
(0.001) (0.001) (0.001) (0.001)
loan_to_deposit -0.000 -0.000 -0.000 -0.000
(0.000) (0.000) (0.000) (0.000)
book_equity_ratio -0.002*** -0.002*** 0.002 -0.002
(0.000) (0.000) (0.003) (0.001)
pct_wholesale_liability 0.004* 0.004* 0.003 0.003
(0.002) (0.002) (0.002) (0.002)
fhlb_ratio 0.005*** 0.005*** 0.005*** 0.006***
(0.002) (0.002) (0.002) (0.002)
roa 0.000 0.000 0.000 0.000
(0.000) (0.000) (0.000) (0.000)
Run Risk (Dummy) 0.014
(0.023)
Adjusted Equity (%) -0.004
(0.003)
Insolvent (s=1.0) -0.076
(0.083)
N 4718 4714 4718 4718
0.107 0.107 0.096 0.094

3.3 Step 2A: Temporal Analysis by OLD Periods

3.3.1 BTFP by OLD Periods (Acute, Post-Acute, Arbitrage)

# ==========================================================================
# STEP 2A: TEMPORAL ANALYSIS - BTFP BY OLD PERIODS (BASELINE 1)
# Acute (Mar 13 - May 1), Post-Acute (May 2 - Oct 31), Arbitrage (Nov 1 - Jan 24)
# ==========================================================================

cat("\n=== STEP 2A: TEMPORAL ANALYSIS - BTFP BY OLD PERIODS (BASELINE 1) ===\n")
#> 
#> === STEP 2A: TEMPORAL ANALYSIS - BTFP BY OLD PERIODS (BASELINE 1) ===
## BTFP Acute period
lpm_btfp_acute_b1 <- feols(
  as.formula(paste("btfp_acute ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## BTFP Post-Acute period
lpm_btfp_post_b1 <- feols(
  as.formula(paste("btfp_post ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## BTFP Arbitrage period
lpm_btfp_arb_b1 <- feols(
  as.formula(paste("btfp_arb ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

modelsummary(
  list("BTFP Acute" = lpm_btfp_acute_b1,
       "BTFP Post-Acute" = lpm_btfp_post_b1,
       "BTFP Arbitrage" = lpm_btfp_arb_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
   
    "pct_mtm_loss" = "MTM Loss (%)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(pct_mtm_loss * uninsured_lev)" = "MTM_Loss × Uninsured",
    "ln_assets" = "Log(Assets)",
    "book_equity_ratio" = "Book Equity Ratio"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.3A: BTFP Usage by OLD Periods (Baseline 1)"
)
Table 3.3A: BTFP Usage by OLD Periods (Baseline 1)
BTFP Acute BTFP Post-Acute BTFP Arbitrage
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.346*** -0.203*** -0.391***
(0.061) (0.063) (0.066)
MTM Loss (%) -0.005 -0.005 0.002
(0.004) (0.004) (0.005)
Uninsured Deposits (%) -0.001* -0.002** -0.001
(0.001) (0.001) (0.001)
MTM_Loss × Uninsured 0.001*** 0.001*** 0.001***
(0.000) (0.000) (0.000)
Log(Assets) 0.033*** 0.025*** 0.038***
(0.004) (0.005) (0.005)
cash_ratio -0.001*** -0.003*** -0.003***
(0.000) (0.001) (0.001)
securities_ratio 0.001** 0.003*** 0.002***
(0.000) (0.000) (0.000)
loan_to_deposit -0.000 0.000* 0.000
(0.000) (0.000) (0.000)
Book Equity Ratio -0.001 -0.002*** -0.000
(0.000) (0.001) (0.001)
pct_wholesale_liability 0.005*** 0.002 0.001
(0.002) (0.002) (0.002)
fhlb_ratio 0.007*** 0.003* 0.007***
(0.001) (0.002) (0.002)
roa -0.000 -0.001 -0.002
(0.001) (0.001) (0.001)
N 4282 4282 4282
0.069 0.051 0.072

3.3.2 DW by OLD Periods

# ==========================================================================
# DW BY OLD PERIODS (BASELINE 1)
# Pre-BTFP, Acute, Post-Acute
# ==========================================================================

cat("\n=== DW BY OLD PERIODS (BASELINE 1) ===\n")
#> 
#> === DW BY OLD PERIODS (BASELINE 1) ===
## DW Pre-BTFP
lpm_dw_pre_b1 <- feols(
  as.formula(paste("dw_pre_btfp ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## DW Acute
lpm_dw_acute_b1 <- feols(
  as.formula(paste("dw_acute ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

## DW Post-Acute
lpm_dw_post_b1 <- feols(
  as.formula(paste("dw_post ~", base_rhs)),
  data = df_1,
  vcov = "hetero"
)

modelsummary(
  list("DW Pre-BTFP" = lpm_dw_pre_b1,
       "DW Acute" = lpm_dw_acute_b1,
       "DW Post-Acute" = lpm_dw_post_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "mtm_other" = "MTM Loss (Other)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.3B: DW Usage by OLD Periods (Baseline 1)",
  notes = "DW data available through Sep 30, 2023 (no Post-Acute data after this date)."
)
Table 3.3B: DW Usage by OLD Periods (Baseline 1)
DW Pre-BTFP DW Acute DW Post-Acute
* p < 0.1, ** p < 0.05, *** p < 0.01
DW data available through Sep 30, 2023 (no Post-Acute data after this date).
(Intercept) -0.744*** -0.464*** -0.578***
(0.067) (0.055) (0.081)
pct_mtm_loss -0.010** -0.005 -0.011**
(0.004) (0.003) (0.005)
Uninsured Deposits (%) -0.002** -0.001 -0.002*
(0.001) (0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.000*** 0.000*** 0.001***
(0.000) (0.000) (0.000)
ln_assets 0.069*** 0.044*** 0.062***
(0.005) (0.004) (0.005)
cash_ratio -0.000 -0.000 -0.001*
(0.001) (0.000) (0.001)
securities_ratio 0.001** 0.000 -0.001
(0.000) (0.000) (0.001)
loan_to_deposit 0.000 -0.000 0.000
(0.000) (0.000) (0.000)
book_equity_ratio -0.000 0.000 0.000
(0.000) (0.000) (0.001)
pct_wholesale_liability 0.004** 0.005** 0.001
(0.002) (0.002) (0.002)
fhlb_ratio 0.002 0.002 0.002
(0.001) (0.001) (0.002)
roa 0.001 -0.000 0.002
(0.002) (0.001) (0.002)
N 4282 4282 4282
0.098 0.066 0.079

3.3.3 OLD Periods - Baseline 2

# ==========================================================================
# OLD PERIODS - BASELINE 2
# ==========================================================================

cat("\n=== OLD PERIODS - BASELINE 2 ===\n")
#> 
#> === OLD PERIODS - BASELINE 2 ===
## BTFP by old periods - Baseline 2
lpm_btfp_acute_b2 <- feols(as.formula(paste("btfp_acute ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_post_b2 <- feols(as.formula(paste("btfp_post ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_arb_b2 <- feols(as.formula(paste("btfp_arb ~", base_rhs)), data = df_2, vcov = "hetero")

modelsummary(
  list("BTFP Acute (B2)" = lpm_btfp_acute_b2,
       "BTFP Post (B2)" = lpm_btfp_post_b2,
       "BTFP Arb (B2)" = lpm_btfp_arb_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.3C: BTFP by OLD Periods (Baseline 2)"
)
Table 3.3C: BTFP by OLD Periods (Baseline 2)
BTFP Acute (B2) BTFP Post (B2) BTFP Arb (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
Uninsured Deposits (%) -0.001* -0.002*** -0.002***
(0.001) (0.001) (0.001)
N 4718 4718 4718
0.069 0.056 0.072
## DW by old periods - Baseline 2
lpm_dw_pre_b2 <- feols(as.formula(paste("dw_pre_btfp ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_dw_acute_b2 <- feols(as.formula(paste("dw_acute ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_dw_post_b2 <- feols(as.formula(paste("dw_post ~", base_rhs)), data = df_2, vcov = "hetero")

modelsummary(
  list("DW Pre (B2)" = lpm_dw_pre_b2,
       "DW Acute (B2)" = lpm_dw_acute_b2,
       "DW Post (B2)" = lpm_dw_post_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.3D: DW by OLD Periods (Baseline 2)"
)
Table 3.3D: DW by OLD Periods (Baseline 2)
DW Pre (B2) DW Acute (B2) DW Post (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.791*** -0.463*** -0.648***
(0.060) (0.049) (0.073)
pct_mtm_loss -0.001 -0.001* -0.001
(0.001) (0.001) (0.001)
Uninsured Deposits (%) -0.001 -0.001 -0.001
(0.001) (0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.000 0.000*** 0.000**
(0.000) (0.000) (0.000)
ln_assets 0.069*** 0.041*** 0.062***
(0.005) (0.004) (0.005)
cash_ratio -0.000 0.000 -0.001
(0.000) (0.000) (0.001)
securities_ratio 0.001*** 0.000 -0.000
(0.000) (0.000) (0.001)
loan_to_deposit 0.000 -0.000 0.000
(0.000) (0.000) (0.000)
book_equity_ratio 0.000 0.000 0.000
(0.000) (0.000) (0.000)
pct_wholesale_liability 0.004** 0.005** 0.001
(0.002) (0.002) (0.002)
fhlb_ratio 0.002 0.002* 0.001
(0.001) (0.001) (0.001)
roa -0.000 -0.000 -0.000
(0.000) (0.000) (0.000)
N 4718 4718 4718
0.102 0.067 0.080

3.4 Step 2B: Temporal Analysis by NEW Periods

3.4.1 BTFP by NEW Periods (P1-P6) - Baseline 1

# ==========================================================================
# STEP 2B: BTFP BY NEW PERIODS (P1-P6) - BASELINE 1
# P1: Week 1, P2: Crisis Month, P3: FRC May, P4: Stabilization,
# P5: Arbitrage, P6: Wind-down
# ==========================================================================

cat("\n=== STEP 2B: BTFP BY NEW PERIODS (BASELINE 1) ===\n")
#> 
#> === STEP 2B: BTFP BY NEW PERIODS (BASELINE 1) ===
## P1: Week 1 (Mar 13-19)
lpm_btfp_p1_b1 <- feols(as.formula(paste("btfp_p1 ~", base_rhs)), data = df_1, vcov = "hetero")

## P2: Crisis Month (Mar 20 - Apr 30)
lpm_btfp_p2_b1 <- feols(as.formula(paste("btfp_p2 ~", base_rhs)), data = df_1, vcov = "hetero")

## P3: FRC May (May 1-31)
lpm_btfp_p3_b1 <- feols(as.formula(paste("btfp_p3 ~", base_rhs)), data = df_1, vcov = "hetero")

## P4: Stabilization (Jun 1 - Sep 30)
lpm_btfp_p4_b1 <- feols(as.formula(paste("btfp_p4 ~", base_rhs)), data = df_1, vcov = "hetero")

## P5: Arbitrage (Nov 1 - Jan 24)
lpm_btfp_p5_b1 <- feols(as.formula(paste("btfp_p5 ~", base_rhs)), data = df_1, vcov = "hetero")

## P6: Wind-down (Jan 25 - Mar 11)
lpm_btfp_p6_b1 <- feols(as.formula(paste("btfp_p6 ~", base_rhs)), data = df_1, vcov = "hetero")

## Display first 3 periods
modelsummary(
  list("P1: Week 1" = lpm_btfp_p1_b1,
       "P2: Crisis Month" = lpm_btfp_p2_b1,
       "P3: FRC May" = lpm_btfp_p3_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_map = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.4A: BTFP by NEW Periods P1-P3 (Baseline 1)"
)
Table 3.4A: BTFP by NEW Periods P1-P3 (Baseline 1)
P1: Week 1 P2: Crisis Month P3: FRC May
* p < 0.1, ** p < 0.05, *** p < 0.01
Uninsured Deposits (%) -0.000 -0.002* -0.001*
(0.000) (0.001) (0.001)
N 4282 4282 4282
0.029 0.060 0.046
## Display last 3 periods
modelsummary(
  list("P4: Stabilization" = lpm_btfp_p4_b1,
       "P5: Arbitrage" = lpm_btfp_p5_b1,
       "P6: Wind-down" = lpm_btfp_p6_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.4B: BTFP by NEW Periods P4-P6 (Baseline 1)"
)
Table 3.4B: BTFP by NEW Periods P4-P6 (Baseline 1)
P4: Stabilization P5: Arbitrage P6: Wind-down
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.063 -0.391*** -0.017
(0.052) (0.066) (0.040)
pct_mtm_loss -0.002 0.002 -0.001
(0.004) (0.005) (0.003)
Uninsured Deposits (%) -0.001 -0.001 0.000
(0.001) (0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.000** 0.001*** 0.000
(0.000) (0.000) (0.000)
ln_assets 0.008** 0.038*** 0.004
(0.004) (0.005) (0.003)
cash_ratio -0.001*** -0.003*** -0.001**
(0.000) (0.001) (0.000)
securities_ratio 0.002*** 0.002*** 0.001***
(0.000) (0.000) (0.000)
loan_to_deposit 0.000** 0.000 -0.000
(0.000) (0.000) (0.000)
book_equity_ratio -0.001*** -0.000 -0.001**
(0.000) (0.001) (0.000)
pct_wholesale_liability -0.000 0.001 0.001
(0.001) (0.002) (0.001)
fhlb_ratio 0.000 0.007*** 0.002*
(0.001) (0.002) (0.001)
roa 0.000 -0.002 -0.001
(0.001) (0.001) (0.001)
N 4282 4282 4282
0.020 0.072 0.014

3.4.2 DW by NEW Periods (P1-P4) - Baseline 1

# ==========================================================================
# DW BY NEW PERIODS (P1-P4) - BASELINE 1
# Note: No DW data for P5-P6 (after Sep 30, 2023)
# ==========================================================================

cat("\n=== DW BY NEW PERIODS (BASELINE 1) ===\n")
#> 
#> === DW BY NEW PERIODS (BASELINE 1) ===
cat("Note: DW data ends Sep 30, 2023 (no data for P5-P6)\n\n")
#> Note: DW data ends Sep 30, 2023 (no data for P5-P6)
## P1: Week 1
lpm_dw_p1_b1 <- feols(as.formula(paste("dw_p1 ~", base_rhs)), data = df_1, vcov = "hetero")

## P2: Crisis Month
lpm_dw_p2_b1 <- feols(as.formula(paste("dw_p2 ~", base_rhs)), data = df_1, vcov = "hetero")

## P3: FRC May
lpm_dw_p3_b1 <- feols(as.formula(paste("dw_p3 ~", base_rhs)), data = df_1, vcov = "hetero")

## P4: Stabilization
lpm_dw_p4_b1 <- feols(as.formula(paste("dw_p4 ~", base_rhs)), data = df_1, vcov = "hetero")

modelsummary(
  list("P1: Week 1" = lpm_dw_p1_b1,
       "P2: Crisis Month" = lpm_dw_p2_b1,
       "P3: FRC May" = lpm_dw_p3_b1,
       "P4: Stabilization" = lpm_dw_p4_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.4C: DW by NEW Periods P1-P4 (Baseline 1)",
  notes = "No DW data available for P5-P6 (after Sep 30, 2023)."
)
Table 3.4C: DW by NEW Periods P1-P4 (Baseline 1)
P1: Week 1 P2: Crisis Month P3: FRC May P4: Stabilization
* p < 0.1, ** p < 0.05, *** p < 0.01
No DW data available for P5-P6 (after Sep 30, 2023).
(Intercept) -0.233*** -0.310*** -0.170*** -0.479***
(0.037) (0.049) (0.045) (0.075)
pct_mtm_loss -0.003 -0.004 -0.003 -0.012***
(0.002) (0.003) (0.003) (0.004)
Uninsured Deposits (%) 0.000 -0.001** -0.001 -0.002
(0.001) (0.001) (0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.000 0.000*** 0.000* 0.000***
(0.000) (0.000) (0.000) (0.000)
ln_assets 0.021*** 0.029*** 0.017*** 0.053***
(0.003) (0.004) (0.003) (0.005)
cash_ratio -0.000* -0.000 -0.000 -0.001*
(0.000) (0.000) (0.000) (0.001)
securities_ratio 0.000 0.000 -0.000 -0.000
(0.000) (0.000) (0.000) (0.001)
loan_to_deposit -0.000 -0.000 0.000 0.000
(0.000) (0.000) (0.000) (0.000)
book_equity_ratio 0.000* 0.000 0.000 -0.000
(0.000) (0.000) (0.000) (0.000)
pct_wholesale_liability 0.002* 0.005*** 0.003** 0.002
(0.001) (0.002) (0.001) (0.002)
fhlb_ratio 0.002* 0.002 0.003** 0.001
(0.001) (0.001) (0.001) (0.001)
roa -0.001 -0.000 -0.000 0.002
(0.000) (0.001) (0.001) (0.002)
N 4282 4282 4282 4282
0.040 0.044 0.027 0.064

3.4.3 NEW Periods - Baseline 2

# ==========================================================================
# NEW PERIODS - BASELINE 2 (ALL BANKS)
# ==========================================================================

cat("\n=== NEW PERIODS - BASELINE 2 ===\n")
#> 
#> === NEW PERIODS - BASELINE 2 ===
## BTFP P1-P3 (Baseline 2)
lpm_btfp_p1_b2 <- feols(as.formula(paste("btfp_p1 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_p2_b2 <- feols(as.formula(paste("btfp_p2 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_p3_b2 <- feols(as.formula(paste("btfp_p3 ~", base_rhs)), data = df_2, vcov = "hetero")

modelsummary(
  list("P1 (B2)" = lpm_btfp_p1_b2, "P2 (B2)" = lpm_btfp_p2_b2, "P3 (B2)" = lpm_btfp_p3_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c("mtm_btfp" = "MTM Loss (BTFP-Eligible)", "uninsured_lev" = "Uninsured Deposits (%)"),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0), list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 3.4D: BTFP P1-P3 (Baseline 2)"
)
Table 3.4D: BTFP P1-P3 (Baseline 2)
P1 (B2) P2 (B2) P3 (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.149*** -0.281*** -0.216***
(0.027) (0.051) (0.044)
pct_mtm_loss -0.000 -0.001* -0.001
(0.000) (0.000) (0.000)
Uninsured Deposits (%) 0.000 -0.001* -0.001**
(0.000) (0.000) (0.000)
I(pct_mtm_loss * uninsured_lev) 0.000 0.000*** 0.000***
(0.000) (0.000) (0.000)
ln_assets 0.013*** 0.026*** 0.021***
(0.002) (0.004) (0.003)
cash_ratio -0.000 -0.001** -0.001***
(0.000) (0.000) (0.000)
securities_ratio 0.000 0.001** 0.001***
(0.000) (0.000) (0.000)
loan_to_deposit -0.000 -0.000 -0.000
(0.000) (0.000) (0.000)
book_equity_ratio 0.000* -0.000 -0.000*
(0.000) (0.000) (0.000)
pct_wholesale_liability 0.002** 0.005*** 0.003**
(0.001) (0.002) (0.001)
fhlb_ratio 0.001* 0.005*** 0.003***
(0.000) (0.001) (0.001)
roa -0.000 0.000 0.000
(0.000) (0.000) (0.000)
N 4718 4718 4718
0.030 0.059 0.046
## BTFP P4-P6 (Baseline 2)
lpm_btfp_p4_b2 <- feols(as.formula(paste("btfp_p4 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_p5_b2 <- feols(as.formula(paste("btfp_p5 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_btfp_p6_b2 <- feols(as.formula(paste("btfp_p6 ~", base_rhs)), data = df_2, vcov = "hetero")

modelsummary(
  list("P4 (B2)" = lpm_btfp_p4_b2, "P5 (B2)" = lpm_btfp_p5_b2, "P6 (B2)" = lpm_btfp_p6_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c("mtm_btfp" = "MTM Loss (BTFP-Eligible)", "uninsured_lev" = "Uninsured Deposits (%)"),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0), list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 3.4E: BTFP P4-P6 (Baseline 2)"
)
Table 3.4E: BTFP P4-P6 (Baseline 2)
P4 (B2) P5 (B2) P6 (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.082* -0.349*** -0.026
(0.044) (0.056) (0.035)
pct_mtm_loss -0.000 -0.000 -0.000
(0.000) (0.001) (0.000)
Uninsured Deposits (%) -0.001* -0.002*** 0.000
(0.000) (0.001) (0.000)
I(pct_mtm_loss * uninsured_lev) 0.000*** 0.001*** 0.000
(0.000) (0.000) (0.000)
ln_assets 0.010*** 0.035*** 0.005*
(0.003) (0.004) (0.003)
cash_ratio -0.001*** -0.002*** -0.000*
(0.000) (0.000) (0.000)
securities_ratio 0.002*** 0.002*** 0.001***
(0.000) (0.000) (0.000)
loan_to_deposit 0.000 -0.000 -0.000
(0.000) (0.000) (0.000)
book_equity_ratio -0.001*** -0.000 -0.001***
(0.000) (0.000) (0.000)
pct_wholesale_liability 0.000 0.001 0.002
(0.001) (0.002) (0.001)
fhlb_ratio 0.000 0.005*** 0.001
(0.001) (0.001) (0.001)
roa 0.000** 0.000 -0.000
(0.000) (0.000) (0.000)
N 4718 4718 4718
0.024 0.072 0.015
## DW P1-P4 (Baseline 2)
lpm_dw_p1_b2 <- feols(as.formula(paste("dw_p1 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_dw_p2_b2 <- feols(as.formula(paste("dw_p2 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_dw_p3_b2 <- feols(as.formula(paste("dw_p3 ~", base_rhs)), data = df_2, vcov = "hetero")
lpm_dw_p4_b2 <- feols(as.formula(paste("dw_p4 ~", base_rhs)), data = df_2, vcov = "hetero")

modelsummary(
  list("P1 (B2)" = lpm_dw_p1_b2, "P2 (B2)" = lpm_dw_p2_b2, "P3 (B2)" = lpm_dw_p3_b2, "P4 (B2)" = lpm_dw_p4_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c("mtm_btfp" = "MTM Loss (BTFP-Eligible)", "uninsured_lev" = "Uninsured Deposits (%)"),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0), list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 3.4F: DW P1-P4 (Baseline 2)"
)
Table 3.4F: DW P1-P4 (Baseline 2)
P1 (B2) P2 (B2) P3 (B2) P4 (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.243*** -0.303*** -0.213*** -0.537***
(0.034) (0.043) (0.040) (0.069)
pct_mtm_loss -0.000 -0.001* -0.000 -0.001
(0.000) (0.000) (0.000) (0.001)
Uninsured Deposits (%) 0.000 -0.001 -0.001 -0.000
(0.000) (0.000) (0.000) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.000 0.000*** 0.000* 0.000
(0.000) (0.000) (0.000) (0.000)
ln_assets 0.020*** 0.026*** 0.020*** 0.051***
(0.003) (0.003) (0.003) (0.005)
cash_ratio -0.000* 0.000 -0.000 -0.001
(0.000) (0.000) (0.000) (0.001)
securities_ratio 0.000 0.000 -0.000 -0.000
(0.000) (0.000) (0.000) (0.001)
loan_to_deposit -0.000 -0.000 -0.000 0.000
(0.000) (0.000) (0.000) (0.000)
book_equity_ratio 0.000*** 0.000 0.000 0.000
(0.000) (0.000) (0.000) (0.000)
pct_wholesale_liability 0.002* 0.005*** 0.003** 0.002
(0.001) (0.002) (0.001) (0.002)
fhlb_ratio 0.001** 0.002* 0.002** 0.001
(0.001) (0.001) (0.001) (0.001)
roa -0.000 -0.000 -0.000 0.000
(0.000) (0.000) (0.000) (0.000)
N 4718 4718 4718 4718
0.043 0.044 0.029 0.062

3.5 Step 3: Intensive Margin Analysis

3.5.1 Step 3A: Intensive Margin (Baseline 1)

# ==========================================================================
# STEP 3A: INTENSIVE MARGIN - BASELINE 1
# Conditional on BTFP usage, how much do banks borrow?
# ==========================================================================

cat("\n=== STEP 3A: INTENSIVE MARGIN (BASELINE 1) ===\n")
#> 
#> === STEP 3A: INTENSIVE MARGIN (BASELINE 1) ===
## Subset to BTFP users only
btfp_users_b1 <- df_1 %>% filter(btfp == 1)
cat("BTFP users in Baseline 1:", nrow(btfp_users_b1), "\n\n")
#> BTFP users in Baseline 1: 1259
## Intensive margin specification
intensive_rhs <- paste(
  "uninsured_lev + eligible_collateral + borrowing_subsidy ",
  controls,
  sep = " + "
)

## Borrowing amount as % of assets
intensive_pct_b1 <- feols(
  as.formula(paste("btfp_amount_pct ~", intensive_rhs)),
  data = btfp_users_b1,
  vcov = "hetero"
)

## With run risk dummy
intensive_rhs_run <- paste(intensive_rhs, "run_risk_1_dummy", sep = " + ")
intensive_run_b1 <- feols(
  as.formula(paste("btfp_amount_pct ~", intensive_rhs_run)),
  data = btfp_users_b1,
  vcov = "hetero"
)

modelsummary(
  list("Main (B1)" = intensive_pct_b1, "Run Risk (B1)" = intensive_run_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename =  c(
    "uninsured_lev" = "Uninsured Deposits (%)",
    "eligible_collateral" = "BTFP-Eligible Collateral (%)",
    "borrowing_subsidy" = "Borrowing Subsidy",
    
    "run_risk_1_dummy" = "Run Risk (Dummy)",
    "ln_assets" = "Log(Assets)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.5A: Intensive Margin - BTFP Borrowing Amount (Baseline 1)",
  notes = "Sample restricted to banks that borrowed from BTFP. Dependent variable: BTFP amount / total assets."
)
Table 3.5A: Intensive Margin - BTFP Borrowing Amount (Baseline 1)
Main (B1) Run Risk (B1)
* p < 0.1, ** p < 0.05, *** p < 0.01
Sample restricted to banks that borrowed from BTFP. Dependent variable: BTFP amount / total assets.
(Intercept) 41.682*** 41.686***
(15.004) (15.013)
Uninsured Deposits (%) 0.145** 0.152**
(0.068) (0.071)
BTFP-Eligible Collateral (%) 0.161* 0.159*
(0.085) (0.086)
Borrowing Subsidy -0.000** -0.000**
(0.000) (0.000)
Log(Assets) -0.425 -0.423
(0.484) (0.483)
cash_ratio -0.625*** -0.632***
(0.180) (0.180)
securities_ratio -0.236 -0.234
(0.181) (0.182)
loan_to_deposit -0.447*** -0.448***
(0.166) (0.166)
book_equity_ratio 0.815*** 0.809***
(0.281) (0.282)
pct_wholesale_liability 0.677*** 0.679***
(0.248) (0.248)
fhlb_ratio 1.176*** 1.178***
(0.296) (0.297)
roa 1.960 1.966
(1.739) (1.740)
Run Risk (Dummy) -0.432
(1.263)
N 1259 1259
0.081 0.081

3.5.2 Step 3B: Intensive Margin (Baseline 2)

# ==========================================================================
# STEP 3B: INTENSIVE MARGIN - BASELINE 2
# ==========================================================================

cat("\n=== STEP 3B: INTENSIVE MARGIN (BASELINE 2) ===\n")
#> 
#> === STEP 3B: INTENSIVE MARGIN (BASELINE 2) ===
btfp_users_b2 <- df_2 %>% filter(btfp == 1)
cat("BTFP users in Baseline 2:", nrow(btfp_users_b2), "\n\n")
#> BTFP users in Baseline 2: 1316
intensive_pct_b2 <- feols(
  as.formula(paste("btfp_amount_pct ~", intensive_rhs)),
  data = btfp_users_b2,
  vcov = "hetero"
)

intensive_run_b2 <- feols(
  as.formula(paste("btfp_amount_pct ~", intensive_rhs_run)),
  data = btfp_users_b2,
  vcov = "hetero"
)

modelsummary(
  list("Main (B2)" = intensive_pct_b2, "Run Risk (B2)" = intensive_run_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename =  c(
    "uninsured_lev" = "Uninsured Deposits (%)",
    "eligible_collateral" = "BTFP-Eligible Collateral (%)",
    "borrowing_subsidy" = "Borrowing Subsidy",
    
    "run_risk_1_dummy" = "Run Risk (Dummy)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.5B: Intensive Margin - BTFP Borrowing Amount (Baseline 2)"
)
Table 3.5B: Intensive Margin - BTFP Borrowing Amount (Baseline 2)
Main (B2) Run Risk (B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) 39.500*** 39.472***
(14.553) (14.556)
Uninsured Deposits (%) 0.151** 0.154**
(0.065) (0.069)
BTFP-Eligible Collateral (%) 0.160* 0.159*
(0.086) (0.086)
Borrowing Subsidy -0.000** -0.000**
(0.000) (0.000)
ln_assets -0.746* -0.745*
(0.400) (0.399)
cash_ratio -0.599*** -0.601***
(0.175) (0.175)
securities_ratio -0.169 -0.168
(0.171) (0.172)
loan_to_deposit -0.381** -0.381**
(0.155) (0.156)
book_equity_ratio 0.761*** 0.759***
(0.277) (0.277)
pct_wholesale_liability 0.633*** 0.633***
(0.244) (0.245)
fhlb_ratio 1.121*** 1.121***
(0.290) (0.290)
roa 2.123 2.124
(1.798) (1.799)
Run Risk (Dummy) -0.172
(1.243)
N 1270 1270
0.080 0.080

3.6 Step 4: Both Banks / Collateral Constraints

3.6.1 Step 4A: Both Banks Analysis (Baseline 1)

# ==========================================================================
# STEP 4A: BOTH BANKS - BASELINE 1
# Among BTFP users, what predicts also using DW?
# ==========================================================================

cat("\n=== STEP 4A: BOTH BANKS ANALYSIS (BASELINE 1) ===\n")
#> 
#> === STEP 4A: BOTH BANKS ANALYSIS (BASELINE 1) ===
## DW usage conditional on BTFP usage
both_rhs <- paste(
  "mtm_btfp + mtm_other + uninsured_lev + I(mtm_btfp * uninsured_lev)",
  controls,
  sep = " + "
)

## Model 1: Include adjusted equity
both_equity_rhs <- paste(both_rhs, "adjusted_equity", sep = " + ")
both_equity_b1 <- feols(
  as.formula(paste("dw ~", both_equity_rhs)),
  data = btfp_users_b1,
  vcov = "hetero"
)

## Model 2: Include maxed out indicator
both_maxout_rhs <- paste(both_rhs, "maxed_out_btfp", sep = " + ")
both_maxout_b1 <- feols(
  as.formula(paste("dw ~", both_maxout_rhs)),
  data = btfp_users_b1,
  vcov = "hetero"
)

modelsummary(
  list("DW | BTFP (Adj. Equity)" = both_equity_b1, "DW | BTFP (Maxed Out)" = both_maxout_b1),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename  = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured",
   
    "maxed_out_btfp" = "Maxed Out BTFP"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.6A: Both Banks Analysis (Baseline 1)",
  notes = "Sample restricted to BTFP users. Dependent variable: indicator for also using DW."
)
Table 3.6A: Both Banks Analysis (Baseline 1)
DW | BTFP (Adj. Equity) DW | BTFP (Maxed Out)
* p < 0.1, ** p < 0.05, *** p < 0.01
Sample restricted to BTFP users. Dependent variable: indicator for also using DW.
(Intercept) -0.973*** -0.946***
(0.338) (0.338)
MTM Loss (BTFP-Eligible) 0.024 0.028
(0.037) (0.035)
mtm_other -0.014 -0.003
(0.017) (0.008)
Uninsured Deposits (%) 0.004** 0.004**
(0.002) (0.002)
MTM_BTFP × Uninsured -0.001 -0.001
(0.001) (0.001)
ln_assets 0.083*** 0.082***
(0.011) (0.011)
cash_ratio -0.000 -0.000
(0.004) (0.004)
securities_ratio -0.000 0.000
(0.004) (0.004)
loan_to_deposit 0.001 0.001
(0.003) (0.003)
book_equity_ratio 0.009 -0.002
(0.018) (0.005)
pct_wholesale_liability 0.009 0.008
(0.006) (0.006)
fhlb_ratio 0.003 0.003
(0.005) (0.005)
roa -0.009 -0.011
(0.027) (0.027)
adjusted_equity -0.011
(0.017)
Maxed Out BTFP -0.021
(0.027)
N 1259 1259
0.103 0.104

3.6.2 Step 4B: Both Banks Analysis (Baseline 2)

# ==========================================================================
# STEP 4B: BOTH BANKS - BASELINE 2
# ==========================================================================

cat("\n=== STEP 4B: BOTH BANKS ANALYSIS (BASELINE 2) ===\n")
#> 
#> === STEP 4B: BOTH BANKS ANALYSIS (BASELINE 2) ===
both_equity_b2 <- feols(
  as.formula(paste("dw ~", both_equity_rhs)),
  data = btfp_users_b2,
  vcov = "hetero"
)

both_maxout_b2 <- feols(
  as.formula(paste("dw ~", both_maxout_rhs)),
  data = btfp_users_b2,
  vcov = "hetero"
)

modelsummary(
  list("DW | BTFP (Adj. Equity, B2)" = both_equity_b2, "DW | BTFP (Maxed Out, B2)" = both_maxout_b2),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured",
   
    "maxed_out_btfp" = "Maxed Out BTFP"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 3.6B: Both Banks Analysis (Baseline 2)"
)
Table 3.6B: Both Banks Analysis (Baseline 2)
DW | BTFP (Adj. Equity, B2) DW | BTFP (Maxed Out, B2)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -1.004*** -0.998***
(0.320) (0.325)
MTM Loss (BTFP-Eligible) 0.014 0.027
(0.036) (0.035)
mtm_other -0.016 -0.004
(0.016) (0.008)
Uninsured Deposits (%) 0.003** 0.004**
(0.002) (0.002)
MTM_BTFP × Uninsured -0.000 -0.001
(0.001) (0.001)
ln_assets 0.085*** 0.083***
(0.010) (0.010)
cash_ratio -0.000 -0.000
(0.004) (0.004)
securities_ratio 0.000 0.001
(0.003) (0.003)
loan_to_deposit 0.001 0.002
(0.003) (0.003)
book_equity_ratio 0.014 -0.002
(0.017) (0.005)
pct_wholesale_liability 0.007 0.008
(0.006) (0.006)
fhlb_ratio 0.003 0.002
(0.004) (0.005)
roa 0.001 -0.012
(0.027) (0.027)
adjusted_equity -0.015
(0.016)
Maxed Out BTFP -0.022
(0.027)
N 1316 1270
0.109 0.115
cat("\n")
cat(strrep("=", 80), "\n")
#> ================================================================================
cat("✓✓✓ PART III COMPLETE: Regression Analysis ✓✓✓\n")
#> ✓✓✓ PART III COMPLETE: Regression Analysis ✓✓✓
cat(strrep("=", 80), "\n")
#> ================================================================================

PART IV: ROBUSTNESS TESTS

This section contains robustness checks and alternative specifications: - Section 4.1: Alternative Samples (excluding large banks, public banks only, etc.) - Section 4.2: Alternative Variable Definitions - Section 4.3: Alternative Period Definitions - Section 4.4: Sample Selection Tests

4.1 Alternative Sample Definitions

4.1.1 Excluding Large Banks (> $100B)

# ==========================================================================
# ROBUSTNESS: EXCLUDE BANKS > $100B ASSETS
# ==========================================================================

cat("\n=== ROBUSTNESS: EXCLUDING LARGE BANKS (>$100B) ===\n")
#> 
#> === ROBUSTNESS: EXCLUDING LARGE BANKS (>$100B) ===
df_small <- df_1 %>%
  filter(assets < 100000000)  # $100B in thousands

cat("Banks remaining:", nrow(df_small), "\n")
#> Banks remaining: 4274
cat("Banks excluded:", nrow(df_1) - nrow(df_small), "\n\n")
#> Banks excluded: 18
lpm_btfp_small <- feols(
  as.formula(paste("btfp ~", base_rhs)),
  data = df_small,
  vcov = "hetero"
)

lpm_dw_small <- feols(
  as.formula(paste("dw ~", base_rhs)),
  data = df_small,
  vcov = "hetero"
)

modelsummary(
  list("BTFP (Baseline)" = lpm_btfp_b1, 
       "BTFP (Small)" = lpm_btfp_small,
       "DW (Baseline)" = lpm_dw_b1,
       "DW (Small)" = lpm_dw_small),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 4.1A: Robustness - Excluding Large Banks (>$100B)"
)
Table 4.1A: Robustness - Excluding Large Banks (>$100B)
BTFP (Baseline) BTFP (Small) DW (Baseline) DW (Small)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -0.519*** -0.842*** -0.870***
(0.079) (0.081) (0.081) (0.083)
pct_mtm_loss -0.005 -0.004 -0.012** -0.013***
(0.005) (0.005) (0.005) (0.005)
Uninsured Deposits (%) -0.004*** -0.004*** -0.003** -0.003***
(0.001) (0.001) (0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.001*** 0.001*** 0.001*** 0.001***
(0.000) (0.000) (0.000) (0.000)
ln_assets 0.055*** 0.057*** 0.089*** 0.092***
(0.006) (0.006) (0.006) (0.006)
cash_ratio -0.003*** -0.003*** -0.001 -0.001
(0.001) (0.001) (0.001) (0.001)
securities_ratio 0.003*** 0.003*** -0.001* -0.001*
(0.001) (0.001) (0.001) (0.001)
loan_to_deposit 0.000 0.000 -0.000 -0.000
(0.000) (0.000) (0.000) (0.000)
book_equity_ratio -0.002*** -0.002*** 0.000 0.000
(0.001) (0.001) (0.001) (0.001)
pct_wholesale_liability 0.003 0.003 0.002 0.002
(0.002) (0.002) (0.002) (0.002)
fhlb_ratio 0.007*** 0.007*** 0.002 0.002
(0.002) (0.002) (0.002) (0.002)
roa -0.001 -0.001 0.001 0.001
(0.001) (0.001) (0.002) (0.002)
N 4282 4264 4282 4264
0.103 0.103 0.121 0.118

4.2 Alternative Variable Definitions

4.2.1 Standardized Variables

# ==========================================================================
# ROBUSTNESS: STANDARDIZED VARIABLES
# ==========================================================================

cat("\n=== ROBUSTNESS: STANDARDIZED VARIABLES ===\n")
#> 
#> === ROBUSTNESS: STANDARDIZED VARIABLES ===
df_std <- df_1 %>%
  mutate(
    mtm_btfp_std = (mtm_btfp - mean(mtm_btfp, na.rm = TRUE)) / sd(mtm_btfp, na.rm = TRUE),
    mtm_other_std = (mtm_other - mean(mtm_other, na.rm = TRUE)) / sd(mtm_other, na.rm = TRUE),
    uninsured_lev_std = (uninsured_lev - mean(uninsured_lev, na.rm = TRUE)) / sd(uninsured_lev, na.rm = TRUE)
  )

base_rhs_std <- paste(
  "mtm_btfp_std + mtm_other_std + uninsured_lev_std + I(mtm_btfp_std * uninsured_lev_std)",
  controls,
  sep = " + "
)

lpm_btfp_std <- feols(
  as.formula(paste("btfp ~", base_rhs_std)),
  data = df_std,
  vcov = "hetero"
)

modelsummary(
  list("Original" = lpm_btfp_b1, "Standardized" = lpm_btfp_std),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "mtm_btfp_std" = "MTM Loss (Std.)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "uninsured_lev_std" = "Uninsured (Std.)",
    "I(mtm_btfp * uninsured_lev)" = "Interaction",
    "I(mtm_btfp_std * uninsured_lev_std)" = "Interaction (Std.)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 4.2A: Robustness - Standardized Variables"
)
Table 4.2A: Robustness - Standardized Variables
Original Standardized
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -0.443***
(0.079) (0.086)
pct_mtm_loss -0.005
(0.005)
Uninsured Deposits (%) -0.004***
(0.001)
I(pct_mtm_loss * uninsured_lev) 0.001***
(0.000)
ln_assets 0.055*** 0.053***
(0.006) (0.006)
cash_ratio -0.003*** -0.004***
(0.001) (0.001)
securities_ratio 0.003*** 0.003***
(0.001) (0.001)
loan_to_deposit 0.000 0.000
(0.000) (0.000)
book_equity_ratio -0.002*** -0.002***
(0.001) (0.001)
pct_wholesale_liability 0.003 0.003
(0.002) (0.002)
fhlb_ratio 0.007*** 0.007***
(0.002) (0.002)
roa -0.001 -0.001
(0.001) (0.002)
MTM Loss (Std.) 0.029***
(0.008)
mtm_other_std 0.021***
(0.007)
Uninsured (Std.) 0.022***
(0.008)
Interaction (Std.) -0.006
(0.006)
N 4282 4282
0.103 0.096

4.2.2 Winsorized Variables

# ==========================================================================
# ROBUSTNESS: WINSORIZED VARIABLES (1% / 99%)
# ==========================================================================

cat("\n=== ROBUSTNESS: WINSORIZED VARIABLES ===\n")
#> 
#> === ROBUSTNESS: WINSORIZED VARIABLES ===
winsorize <- function(x, prob = c(0.01, 0.99)) {
  limits <- quantile(x, probs = prob, na.rm = TRUE)
  x[x < limits[1]] <- limits[1]
  x[x > limits[2]] <- limits[2]
  return(x)
}

df_wins <- df_1 %>%
  mutate(
    mtm_btfp_w = winsorize(mtm_btfp),
    mtm_other_w = winsorize(mtm_other),
    uninsured_lev_w = winsorize(uninsured_lev)
  )

base_rhs_w <- paste(
  "mtm_btfp_w + mtm_other_w + uninsured_lev_w + I(mtm_btfp_w * uninsured_lev_w)",
  controls,
  sep = " + "
)

lpm_btfp_wins <- feols(
  as.formula(paste("btfp ~", base_rhs_w)),
  data = df_wins,
  vcov = "hetero"
)

modelsummary(
  list("Original" = lpm_btfp_b1, "Winsorized" = lpm_btfp_wins),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss",
    "mtm_btfp_w" = "MTM Loss (Wins.)",
    "uninsured_lev" = "Uninsured",
    "uninsured_lev_w" = "Uninsured (Wins.)"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 4.2B: Robustness - Winsorized Variables (1%/99%)"
)
Table 4.2B: Robustness - Winsorized Variables (1%/99%)
Original Winsorized
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -0.574***
(0.079) (0.082)
pct_mtm_loss -0.005
(0.005)
Uninsured -0.004***
(0.001)
I(pct_mtm_loss * uninsured_lev) 0.001***
(0.000)
ln_assets 0.055*** 0.052***
(0.006) (0.006)
cash_ratio -0.003*** -0.004***
(0.001) (0.001)
securities_ratio 0.003*** 0.003***
(0.001) (0.001)
loan_to_deposit 0.000 0.000
(0.000) (0.000)
book_equity_ratio -0.002*** -0.002***
(0.001) (0.001)
pct_wholesale_liability 0.003 0.003
(0.002) (0.002)
fhlb_ratio 0.007*** 0.008***
(0.002) (0.002)
roa -0.001 -0.000
(0.001) (0.001)
MTM Loss (Wins.) 0.066***
(0.020)
mtm_other_w 0.011***
(0.004)
Uninsured (Wins.) 0.003***
(0.001)
I(mtm_btfp_w * uninsured_lev_w) -0.001
(0.001)
N 4282 4282
0.103 0.097

4.3 Alternative Specifications

4.3.1 Probit Models

# ==========================================================================
# ROBUSTNESS: PROBIT INSTEAD OF LPM
# ==========================================================================

cat("\n=== ROBUSTNESS: PROBIT MODELS ===\n")
#> 
#> === ROBUSTNESS: PROBIT MODELS ===
probit_btfp <- glm(
  as.formula(paste("btfp ~", base_rhs)),
  data = df_1,
  family = binomial(link = "probit")
)

probit_dw <- glm(
  as.formula(paste("dw ~", base_rhs)),
  data = df_1,
  family = binomial(link = "probit")
)

modelsummary(
  list("LPM (BTFP)" = lpm_btfp_b1, 
       "Probit (BTFP)" = probit_btfp,
       "LPM (DW)" = lpm_dw_b1,
       "Probit (DW)" = probit_dw),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3),
    list("raw" = "pseudo.r.squared", "clean" = "Pseudo R²", "fmt" = 3)
  ),
  title = "Table 4.3A: Robustness - Probit vs LPM"
)
Table 4.3A: Robustness - Probit vs LPM
LPM (BTFP) Probit (BTFP) LPM (DW) Probit (DW)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -3.386*** -0.842*** -4.337***
(0.079) (0.345) (0.081) (0.333)
pct_mtm_loss -0.005 0.011 -0.012** -0.034
(0.005) (0.022) (0.005) (0.022)
Uninsured Deposits (%) -0.004*** -0.003 -0.003** -0.007
(0.001) (0.005) (0.001) (0.005)
I(pct_mtm_loss * uninsured_lev) 0.001*** 0.002** 0.001*** 0.002***
(0.000) (0.001) (0.000) (0.001)
ln_assets 0.055*** 0.163*** 0.089*** 0.288***
(0.006) (0.018) (0.006) (0.018)
cash_ratio -0.003*** -0.015*** -0.001 -0.005
(0.001) (0.004) (0.001) (0.004)
securities_ratio 0.003*** 0.015*** -0.001* -0.004
(0.001) (0.003) (0.001) (0.003)
loan_to_deposit 0.000 0.008*** -0.000 0.002
(0.000) (0.003) (0.000) (0.002)
book_equity_ratio -0.002*** -0.039*** 0.000 -0.013**
(0.001) (0.007) (0.001) (0.006)
pct_wholesale_liability 0.003 0.006 0.002 0.007
(0.002) (0.007) (0.002) (0.007)
fhlb_ratio 0.007*** 0.009 0.002 0.002
(0.002) (0.006) (0.002) (0.006)
roa -0.001 -0.033 0.001 0.016
(0.001) (0.027) (0.002) (0.016)
N 4282 4282 4282 4282
0.103 0.121

4.3.2 Clustered Standard Errors (by Size Bin)

# ==========================================================================
# ROBUSTNESS: CLUSTERED STANDARD ERRORS BY SIZE BIN
# ==========================================================================

cat("\n=== ROBUSTNESS: CLUSTERED STANDARD ERRORS ===\n")
#> 
#> === ROBUSTNESS: CLUSTERED STANDARD ERRORS ===
lpm_btfp_cluster <- feols(
  as.formula(paste("btfp ~", base_rhs)),
  data = df_1,
  cluster = ~size_bin
)

lpm_dw_cluster <- feols(
  as.formula(paste("dw ~", base_rhs)),
  data = df_1,
  cluster = ~size_bin
)

modelsummary(
  list("Robust SE" = lpm_btfp_b1, 
       "Clustered SE" = lpm_btfp_cluster),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "uninsured_lev" = "Uninsured Deposits (%)",
    "I(mtm_btfp * uninsured_lev)" = "MTM_BTFP × Uninsured"
  ),
  gof_map = list(
    list("raw" = "nobs", "clean" = "N", "fmt" = 0),
    list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)
  ),
  title = "Table 4.3B: Robustness - Clustered Standard Errors (by Size Bin)"
)
Table 4.3B: Robustness - Clustered Standard Errors (by Size Bin)
Robust SE Clustered SE
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.497*** -0.497
(0.079) (0.206)
pct_mtm_loss -0.005 -0.005
(0.005) (0.001)
Uninsured Deposits (%) -0.004*** -0.004
(0.001) (0.001)
I(pct_mtm_loss * uninsured_lev) 0.001*** 0.001*
(0.000) (0.000)
ln_assets 0.055*** 0.055
(0.006) (0.021)
cash_ratio -0.003*** -0.003
(0.001) (0.001)
securities_ratio 0.003*** 0.003**
(0.001) (0.000)
loan_to_deposit 0.000 0.000
(0.000) (0.000)
book_equity_ratio -0.002*** -0.002**
(0.001) (0.000)
pct_wholesale_liability 0.003 0.003
(0.002) (0.002)
fhlb_ratio 0.007*** 0.007
(0.002) (0.004)
roa -0.001 -0.001*
(0.001) (0.000)
N 4282 4282
0.103 0.103

PART V: VISUALIZATIONS

This section contains all key figures and visualizations: - Section 5.1: Historic DW Borrowing Pattern - Section 5.2: March 2023 Daily Analysis - Section 5.3: Daily Borrowing with Period Shading - Section 5.4: Interest Rate Dynamics - Section 5.5: User Group Analysis - Section 5.6: Collateral and Utilization Analysis - Section 5.7: Regression-Related Visualizations

5.1 Figure 1: Historic DW Borrowing Pattern (Mar 2022 - Sep 2023)

# ==========================================================================
# FIGURE 1: HISTORIC DW BORROWING (MONTHLY, ALL BANKS)
# March 2022 to September 2023 - LINE GRAPH
# ==========================================================================

cat("\n=== PART V: VISUALIZATIONS ===\n")
#> 
#> === PART V: VISUALIZATIONS ===
## Aggregate DW loans monthly (all banks)
dw_monthly <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2022-03-01"), 
         dw_loan_date <= as.Date("2023-09-30")) %>%
  mutate(month = floor_date(dw_loan_date, "month")) %>%
  group_by(month) %>%
  summarise(
    total_amount = sum(dw_loan_amount, na.rm = TRUE) / 1e9,
    n_loans = n(),
    n_banks = n_distinct(rssd_id),
    avg_rate = weighted.mean(dw_interest_rate, dw_loan_amount, na.rm = TRUE),
    .groups = "drop"
  )

p_fig01 <- ggplot(dw_monthly, aes(x = month)) +
  # Amount line
  geom_line(aes(y = total_amount), color = facility_colors["DW"], linewidth = 1.2) +
  geom_point(aes(y = total_amount), color = facility_colors["DW"], size = 2.5) +
  # BTFP launch event

  geom_vline(xintercept = as.Date("2023-03-13"), linetype = "dashed", color = "#2166ac", linewidth = 0.8) +
  annotate("text", x = as.Date("2023-03-13"), y = max(dw_monthly$total_amount) * 0.95,
           label = "BTFP Launch", hjust = -0.1, size = 3.5, fontface = "bold") +
  scale_x_date(date_breaks = "2 months", date_labels = "%b %Y") +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B")) +
  labs(
    title = "Figure 1: Historic Discount Window Borrowing",
    subtitle = "Monthly total borrowing volume (March 2022 - September 2023)",
    x = NULL, y = "Total DW Borrowing"
  ) +
  theme_pub()

print(p_fig01)

ggsave(file.path(FIG_PATH, "fig01_historic_dw_monthly.png"), p_fig01, width = 14, height = 8, dpi = 300, bg = "white")

5.2 Figure 2: Daily Loans in March 2023

# ==========================================================================
# FIGURE 2: DAILY LOANS FOR MARCH 2023 (ALL 31 DAYS)
# LINE GRAPH with event markers
# ==========================================================================

march_start <- as.Date("2023-03-01")
march_end <- as.Date("2023-03-31")

## BTFP daily
btfp_march <- btfp_raw %>%
  filter(btfp_loan_date >= march_start, btfp_loan_date <= march_end) %>%
  group_by(date = btfp_loan_date) %>%
  summarise(amount = sum(btfp_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "BTFP")

## DW daily
dw_march <- dw_raw %>%
  filter(dw_loan_date >= march_start, dw_loan_date <= march_end) %>%
  group_by(date = dw_loan_date) %>%
  summarise(amount = sum(dw_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "DW")

## Combine and complete dates
all_march_dates <- tibble(date = seq(march_start, march_end, by = "day"))
march_daily <- bind_rows(btfp_march, dw_march) %>%
  complete(date = all_march_dates$date, facility, fill = list(amount = 0)) %>%
  filter(!is.na(facility))

## Events
march_events <- tibble(
  date = as.Date(c("2023-03-10", "2023-03-12", "2023-03-13")),
  label = c("SVB Fails", "Signature Fails", "BTFP Launch")
)

p_fig02 <- ggplot(march_daily, aes(x = date, y = amount, color = facility)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  # Event lines
  geom_vline(data = march_events, aes(xintercept = date), 
             linetype = "dashed", color = "gray40", linewidth = 0.7) +
  geom_label(data = march_events, aes(x = date, y = max(march_daily$amount) * 0.9, label = label),
             inherit.aes = FALSE, size = 3, hjust = 0, label.size = 0, fill = "white") +
  scale_color_manual(values = facility_colors[c("BTFP", "DW")]) +
  scale_x_date(date_breaks = "3 days", date_labels = "%b %d") +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B")) +
  labs(
    title = "Figure 2: Daily Emergency Borrowing in March 2023",
    subtitle = "All banks | Key crisis events marked",
    x = NULL, y = "Daily Borrowing Volume", color = "Facility"
  ) +
  theme_pub() +
  theme(legend.position = c(0.85, 0.85))

print(p_fig02)

ggsave(file.path(FIG_PATH, "fig02_march_daily.png"), p_fig02, width = 14, height = 8, dpi = 300, bg = "white")

5.3 Figure 3: Daily Borrowing with Period Shading (Mar 2023 - Mar 2024)

# ==========================================================================
# FIGURE 3: DAILY BORROWING (MAR 1, 2023 - MAR 11, 2024)
# Excluding failed banks and GSIBs
# NEW period definition shading, event lines
# ==========================================================================

## Get valid bank IDs (exclude failed and GSIB)
valid_banks <- baseline_1$idrssd

## BTFP daily
btfp_daily <- btfp_raw %>%
  filter(btfp_loan_date >= as.Date("2023-03-01"),
         btfp_loan_date <= BTFP_CLOSE,
         rssd_id %in% valid_banks) %>%
  group_by(date = btfp_loan_date) %>%
  summarise(amount = sum(btfp_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "BTFP")

## DW daily
dw_daily <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2023-03-01"),
         dw_loan_date <= DW_DATA_END,
         rssd_id %in% valid_banks) %>%
  group_by(date = dw_loan_date) %>%
  summarise(amount = sum(dw_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(facility = "DW")

daily_combined <- bind_rows(btfp_daily, dw_daily)

## NEW Period rectangles
period_shading <- tibble(
  xmin = as.Date(c("2023-03-13", "2023-03-20", "2023-05-01", "2023-06-01", "2023-11-01", "2024-01-25")),
  xmax = as.Date(c("2023-03-19", "2023-04-30", "2023-05-31", "2023-09-30", "2024-01-24", "2024-03-11")),
  period = c("P1: Week 1", "P2: Crisis Month", "P3: FRC May", 
             "P4: Stabilization", "P5: Arbitrage", "P6: Wind-down"),
  fill = c("#d73027", "#fc8d59", "#fee08b", "#d9ef8b", "#91bfdb", "#4575b4")
)

## Key events
key_events <- tibble(
  date = as.Date(c("2023-03-10", "2023-05-01", "2023-09-30", "2023-11-06", "2024-01-24")),
  label = c("SVB Fails", "FRC Fails", "DW Data Ends", "Arb Window Opens", "BTFP Closes")
)

p_fig03 <- ggplot() +
  # Period shading (use annotate for cleaner approach)
  annotate("rect", xmin = period_shading$xmin, xmax = period_shading$xmax,
           ymin = 0, ymax = Inf, fill = period_shading$fill, alpha = 0.25) +
  # Daily borrowing lines
  geom_line(data = daily_combined, aes(x = date, y = amount, color = facility), linewidth = 0.8) +
  # Event lines
  geom_vline(data = key_events, aes(xintercept = date), 
             linetype = "dashed", color = "gray30", linewidth = 0.5) +
  # Event labels
  geom_text(data = key_events, aes(x = date, y = max(daily_combined$amount, na.rm = TRUE) * 1.05, label = label),
            angle = 90, hjust = 0, vjust = 0.5, size = 2.8) +
  scale_color_manual(values = facility_colors[c("BTFP", "DW")], name = "Facility") +
  scale_x_date(date_breaks = "1 month", date_labels = "%b\n%Y", 
               limits = c(as.Date("2023-03-01"), BTFP_CLOSE)) +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B"), expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Figure 3: Daily Emergency Borrowing by Crisis Period",
    subtitle = "March 2023 - March 2024 | Excludes GSIBs and failed banks | Shading = NEW period definitions",
    x = NULL, y = "Daily Borrowing Volume",
    caption = "Periods: P1=Week 1 (red), P2=Crisis Month (orange), P3=FRC May (yellow), P4=Stabilization (green), P5=Arbitrage (blue), P6=Wind-down (dark blue)"
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

print(p_fig03)

ggsave(file.path(FIG_PATH, "fig03_daily_with_periods.png"), p_fig03, width = 16, height = 9, dpi = 300, bg = "white")

5.4 Figure 4: Interest Rate Dynamics (Mar 2023 - Mar 2024)

# ==========================================================================
# FIGURE 4: INTEREST RATE DIFFERENCES
# March 1, 2023 - March 11, 2024
# NEW period shading, event lines
# ==========================================================================

## Weekly average rates for BTFP
btfp_rates <- btfp_raw %>%
  filter(btfp_loan_date >= as.Date("2023-03-01"),
         btfp_loan_date <= BTFP_CLOSE) %>%
  mutate(week = floor_date(btfp_loan_date, "week")) %>%
  group_by(week) %>%
  summarise(
    avg_rate = weighted.mean(btfp_interest_rate, btfp_loan_amount, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(facility = "BTFP")

## Weekly average rates for DW
dw_rates <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2023-03-01"),
         dw_loan_date <= DW_DATA_END) %>%
  mutate(week = floor_date(dw_loan_date, "week")) %>%
  group_by(week) %>%
  summarise(
    avg_rate = weighted.mean(dw_interest_rate, dw_loan_amount, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(facility = "DW")

weekly_rates <- bind_rows(btfp_rates, dw_rates)

## Rate events
rate_events <- tibble(
  date = as.Date(c("2023-03-13", "2023-05-01", "2023-11-06")),
  label = c("BTFP Launch", "FRC Fails", "Arb Window Opens")
)

p_fig04 <- ggplot() +
  # Period shading using annotate
  annotate("rect", 
           xmin = (period_shading %>% filter(xmax <= as.Date("2024-03-11")))$xmin,
           xmax = (period_shading %>% filter(xmax <= as.Date("2024-03-11")))$xmax,
           ymin = -Inf, ymax = Inf, 
           fill = (period_shading %>% filter(xmax <= as.Date("2024-03-11")))$fill,
           alpha = 0.2) +
  # Rate lines
  geom_line(data = weekly_rates, aes(x = week, y = avg_rate, color = facility), linewidth = 1.2) +
  geom_point(data = weekly_rates, aes(x = week, y = avg_rate, color = facility), size = 2) +
  # Event lines
  geom_vline(data = rate_events, aes(xintercept = date), 
             linetype = "dashed", color = "gray30", linewidth = 0.5) +
  geom_text(data = rate_events, aes(x = date, y = max(weekly_rates$avg_rate, na.rm = TRUE) * 1.02, label = label),
            angle = 90, hjust = 0, vjust = 0.5, size = 2.8) +
  scale_color_manual(values = facility_colors[c("BTFP", "DW")], name = "Facility") +
  scale_x_date(date_breaks = "1 month", date_labels = "%b\n%Y") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  labs(
    title = "Figure 4: Weekly Average Interest Rates by Facility",
    subtitle = "Weighted by loan amount | March 2023 - March 2024",
    x = NULL, y = "Average Interest Rate",
    caption = "Shading: P1=Week 1, P2=Crisis Month, P3=FRC May, P4=Stabilization, P5=Arbitrage, P6=Wind-down"
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

print(p_fig04)

ggsave(file.path(FIG_PATH, "fig04_rate_dynamics.png"), p_fig04, width = 16, height = 9, dpi = 300, bg = "white")

5.5 Figure 5: Borrowing by User Group (Jan - Sep 2023)

# ==========================================================================
# FIGURE 5: BORROWING BY USER GROUP (DW Only, BTFP Only, Both)
# January 1, 2023 - September 30, 2023
# NEW period shading, event lines
# ==========================================================================

## Classify banks by facility usage (from df_1)
bank_groups <- df_1 %>%
  select(idrssd, facility) %>%
  filter(facility != "Neither")

## Weekly BTFP by user group
btfp_by_group <- btfp_raw %>%
  filter(btfp_loan_date >= as.Date("2023-01-01"),
         btfp_loan_date <= as.Date("2023-09-30"),
         rssd_id %in% valid_banks) %>%
  left_join(bank_groups %>% rename(rssd_id = idrssd), by = "rssd_id") %>%
  filter(!is.na(facility)) %>%
  mutate(week = floor_date(btfp_loan_date, "week")) %>%
  group_by(week, facility) %>%
  summarise(amount = sum(btfp_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(loan_type = "BTFP Loans")

## Weekly DW by user group
dw_by_group <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2023-01-01"),
         dw_loan_date <= as.Date("2023-09-30"),
         rssd_id %in% valid_banks) %>%
  left_join(bank_groups %>% rename(rssd_id = idrssd), by = "rssd_id") %>%
  filter(!is.na(facility)) %>%
  mutate(week = floor_date(dw_loan_date, "week")) %>%
  group_by(week, facility) %>%
  summarise(amount = sum(dw_loan_amount, na.rm = TRUE) / 1e9, .groups = "drop") %>%
  mutate(loan_type = "DW Loans")

## Combine
group_weekly <- bind_rows(btfp_by_group, dw_by_group) %>%
  mutate(group_label = paste(facility, "-", loan_type))

## Subset period shading for this date range
period_shading_sub <- period_shading %>%
  filter(xmin >= as.Date("2023-01-01"), xmax <= as.Date("2023-09-30"))

## Events
group_events <- tibble(
  date = as.Date(c("2023-03-13", "2023-05-01")),
  label = c("BTFP Launch", "FRC Fails")
)

p_fig05 <- ggplot() +
  # Period shading using annotate
  annotate("rect", xmin = period_shading_sub$xmin, xmax = period_shading_sub$xmax,
           ymin = 0, ymax = Inf, fill = period_shading_sub$fill, alpha = 0.2) +
  # Lines by group
  geom_line(data = group_weekly, 
            aes(x = week, y = amount, color = facility, linetype = loan_type), 
            linewidth = 1) +
  # Event lines
  geom_vline(data = group_events, aes(xintercept = date), 
             linetype = "dashed", color = "gray30", linewidth = 0.5) +
  geom_text(data = group_events, 
            aes(x = date, y = max(group_weekly$amount, na.rm = TRUE) * 1.05, label = label),
            angle = 90, hjust = 0, vjust = 0.5, size = 3) +
  scale_color_manual(values = c("BTFP Only" = "#2166ac", "DW Only" = "#b2182b", "Both" = "#762a83"), 
                     name = "User Group") +
  scale_linetype_manual(values = c("BTFP Loans" = "solid", "DW Loans" = "dashed"), name = "Loan Type") +
  scale_x_date(date_breaks = "1 month", date_labels = "%b\n%Y",
               limits = c(as.Date("2023-01-01"), as.Date("2023-09-30"))) +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B"), expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Figure 5: Weekly Borrowing by User Group",
    subtitle = "January - September 2023 | Banks classified by full-period facility usage",
    x = NULL, y = "Weekly Borrowing Volume"
  ) +
  theme_pub() +
  theme(legend.position = "bottom", legend.box = "horizontal")

print(p_fig05)

ggsave(file.path(FIG_PATH, "fig05_user_groups.png"), p_fig05, width = 16, height = 9, dpi = 300, bg = "white")

5.6 Figure 6: Collateral and Utilization Analysis

# ==========================================================================
# FIGURE 6: PLEDGED COLLATERAL TYPES VS UTILIZATION
# January 1, 2023 - March 11, 2024
# 
# Collateral variables:
#   btfp_total_collateral = BTFP-eligible (OMO)
#   dw_omo_eligible = BTFP-eligible pledged at DW
#   dw_non_omo_eligible = Other collateral (DW only, subject to haircut)
#
# Par value, no haircut applied
# ==========================================================================

## Panel A: DW Collateral Composition (OMO vs Non-OMO)
dw_coll_weekly <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2023-01-01"),
         dw_loan_date <= DW_DATA_END) %>%
  mutate(week = floor_date(dw_loan_date, "week")) %>%
  group_by(week) %>%
  summarise(
    omo_eligible = sum(dw_omo_eligible, na.rm = TRUE) / 1e9,
    non_omo_eligible = sum(dw_non_omo_eligible, na.rm = TRUE) / 1e9,
    total_collateral = sum(dw_total_collateral, na.rm = TRUE) / 1e9,
    loan_amount = sum(dw_loan_amount, na.rm = TRUE) / 1e9,
    .groups = "drop"
  ) %>%
  mutate(utilization = loan_amount / total_collateral * 100)

## Reshape for stacked area
dw_coll_long <- dw_coll_weekly %>%
  select(week, omo_eligible, non_omo_eligible) %>%
  pivot_longer(cols = c(omo_eligible, non_omo_eligible),
               names_to = "collateral_type", values_to = "amount") %>%
  mutate(collateral_type = factor(collateral_type,
                                   levels = c("non_omo_eligible", "omo_eligible"),
                                   labels = c("Non-OMO (DW Only, w/ Haircut)", "OMO-Eligible (BTFP-Eligible)")))

p_coll_a <- ggplot(dw_coll_long, aes(x = week, y = amount, fill = collateral_type)) +
  geom_area(alpha = 0.8) +
  geom_vline(xintercept = as.Date("2023-03-13"), linetype = "dashed", color = "gray30") +
  annotate("text", x = as.Date("2023-03-13"), y = max(dw_coll_weekly$total_collateral) * 0.9,
           label = "BTFP Launch", hjust = -0.1, size = 3.5) +
  scale_fill_manual(values = c("OMO-Eligible (BTFP-Eligible)" = "#2166ac", 
                                "Non-OMO (DW Only, w/ Haircut)" = "#b2182b")) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B")) +
  labs(
    title = "A: DW Collateral Composition (Par Value)",
    subtitle = "Weekly pledged collateral by type | No haircuts applied",
    x = NULL, y = "Collateral Pledged", fill = "Collateral Type"
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

## Panel B: BTFP Collateral and Utilization
btfp_coll_weekly <- btfp_raw %>%
  filter(btfp_loan_date >= as.Date("2023-01-01"),
         btfp_loan_date <= BTFP_CLOSE) %>%
  mutate(week = floor_date(btfp_loan_date, "week")) %>%
  group_by(week) %>%
  summarise(
    collateral = sum(btfp_total_collateral, na.rm = TRUE) / 1e9,
    loan_amount = sum(btfp_loan_amount, na.rm = TRUE) / 1e9,
    .groups = "drop"
  ) %>%
  mutate(utilization = loan_amount / collateral * 100)

p_coll_b <- ggplot(btfp_coll_weekly, aes(x = week)) +
  # Collateral line
  geom_line(aes(y = collateral, color = "Collateral Pledged"), linewidth = 1.2) +
  geom_point(aes(y = collateral, color = "Collateral Pledged"), size = 2) +
  # Loan amount line
  geom_line(aes(y = loan_amount, color = "Loan Amount"), linewidth = 1.2) +
  geom_point(aes(y = loan_amount, color = "Loan Amount"), size = 2) +
  # Arbitrage window
  geom_vline(xintercept = ARB_WINDOW_OPEN, linetype = "dashed", color = "#762a83", linewidth = 0.8) +
  annotate("text", x = ARB_WINDOW_OPEN, y = max(btfp_coll_weekly$collateral) * 0.95,
           label = "Arbitrage Window Opens", hjust = -0.05, size = 3.5, color = "#762a83") +
  scale_color_manual(values = c("Collateral Pledged" = "#2166ac", "Loan Amount" = "#d73027")) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b %Y") +
  scale_y_continuous(labels = scales::dollar_format(suffix = "B")) +
  labs(
    title = "B: BTFP Collateral and Borrowing (Par Value)",
    subtitle = "Weekly pledged collateral vs loan amounts",
    x = NULL, y = "Amount", color = NULL
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

## Panel C: Utilization Rates by Facility
util_combined <- bind_rows(
  btfp_coll_weekly %>% select(week, utilization) %>% mutate(facility = "BTFP"),
  dw_coll_weekly %>% select(week, utilization) %>% mutate(facility = "DW")
)

p_coll_c <- ggplot(util_combined, aes(x = week, y = utilization, color = facility)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  geom_hline(yintercept = 100, linetype = "dotted", color = "gray50") +
  annotate("text", x = min(util_combined$week), y = 102, label = "100% Utilization", 
           hjust = 0, size = 3, color = "gray50") +
  # Arbitrage window
  geom_vline(xintercept = ARB_WINDOW_OPEN, linetype = "dashed", color = "#762a83", linewidth = 0.8) +
  scale_color_manual(values = facility_colors[c("BTFP", "DW")]) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b %Y") +
  scale_y_continuous(labels = function(x) paste0(x, "%"), limits = c(0, NA)) +
  labs(
    title = "C: Collateral Utilization Rates",
    subtitle = "Loan Amount / Collateral Pledged",
    x = NULL, y = "Utilization Rate", color = "Facility"
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

## Combine panels
p_fig06 <- (p_coll_a / p_coll_b / p_coll_c) +
  plot_annotation(
    title = "Figure 6: Collateral Analysis by Facility",
    subtitle = "January 2023 - March 2024 | Par value (no haircuts)",
    theme = theme(plot.title = element_text(face = "bold", size = 14),
                  plot.subtitle = element_text(size = 11))
  )

print(p_fig06)

ggsave(file.path(FIG_PATH, "fig06_collateral_analysis.png"), p_fig06, width = 16, height = 12, dpi = 300, bg = "white")

PART VI: FACILITY DESIGN CHOICE MODELS

This section tests whether facility design features drive bank selection behavior.

Feature BTFP Discount Window
Maximum Term 1 year (365 days) Typically 90 days
Collateral Valuation Par value Market value (with haircuts)
Rate OIS + 10bp (fixed) Primary Credit Rate
Stigma Lower (new program) Higher (traditional)

6.1 Aggregate Facility Design Variables

# ==========================================================================
# AGGREGATE FACILITY DESIGN VARIABLES FROM LOAN-LEVEL TO BANK-LEVEL
#
# Loan-level variables (exact names):
#   BTFP: btfp_loan_date, btfp_maturity_date, btfp_term, btfp_repayment_date,
#         btfp_loan_amount, btfp_interest_rate, btfp_total_collateral,
#         btfp_effective_maturity_days
#
#   DW:   dw_loan_date, dw_maturity_date, dw_term, dw_repayment_date,
#         dw_interest_rate, dw_loan_amount, dw_total_collateral,
#         dw_effective_maturity_days, dw_omo_eligible, dw_non_omo_eligible
# ==========================================================================

cat("\n=== PART VI: FACILITY DESIGN CHOICE MODELS ===\n")
#> 
#> === PART VI: FACILITY DESIGN CHOICE MODELS ===
cat("\n=== Aggregating Facility Design Variables ===\n")
#> 
#> === Aggregating Facility Design Variables ===
## BTFP Design Aggregation
btfp_design <- btfp_raw %>%
  filter(btfp_loan_date >= CRISIS_START, btfp_loan_date <= BTFP_CLOSE) %>%
  group_by(rssd_id) %>%
  summarise(
    # Term
    btfp_avg_term = weighted.mean(btfp_term, btfp_loan_amount, na.rm = TRUE),
    btfp_max_term = max(btfp_term, na.rm = TRUE),
    btfp_min_term = min(btfp_term, na.rm = TRUE),
    # Rate
    btfp_avg_rate = weighted.mean(btfp_interest_rate, btfp_loan_amount, na.rm = TRUE),
    btfp_max_rate = max(btfp_interest_rate, na.rm = TRUE),
    # Effective maturity
    btfp_avg_eff_mat = weighted.mean(btfp_effective_maturity_days, btfp_loan_amount, na.rm = TRUE),
    # Collateral
    btfp_total_coll = sum(btfp_total_collateral, na.rm = TRUE),
    # Counts
    btfp_n_loans = n(),
    btfp_total_amt = sum(btfp_loan_amount, na.rm = TRUE),
    # Term choice
    btfp_used_full_term = as.integer(any(btfp_term >= 360, na.rm = TRUE)),
    btfp_pct_early_repay = mean(btfp_effective_maturity_days < btfp_term, na.rm = TRUE) * 100,
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

cat("✓ BTFP design aggregated:", nrow(btfp_design), "banks\n")
#> ✓ BTFP design aggregated: 1327 banks
## DW Design Aggregation
dw_design <- dw_raw %>%
  filter(dw_loan_date >= PRE_BTFP_START, dw_loan_date <= DW_DATA_END) %>%
  group_by(rssd_id) %>%
  summarise(
    # Term
    dw_avg_term = weighted.mean(dw_term, dw_loan_amount, na.rm = TRUE),
    dw_max_term = max(dw_term, na.rm = TRUE),
    # Rate
    dw_avg_rate = weighted.mean(dw_interest_rate, dw_loan_amount, na.rm = TRUE),
    dw_max_rate = max(dw_interest_rate, na.rm = TRUE),
    # Effective maturity
    dw_avg_eff_mat = weighted.mean(dw_effective_maturity_days, dw_loan_amount, na.rm = TRUE),
    # Collateral
    dw_total_coll = sum(dw_total_collateral, na.rm = TRUE),
    dw_omo_coll = sum(dw_omo_eligible, na.rm = TRUE),
    dw_non_omo_coll = sum(dw_non_omo_eligible, na.rm = TRUE),
    dw_pct_omo_coll = dw_omo_coll / dw_total_coll * 100,
    # Counts
    dw_n_loans = n(),
    dw_total_amt = sum(dw_loan_amount, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

cat("✓ DW design aggregated:", nrow(dw_design), "banks\n")
#> ✓ DW design aggregated: 1384 banks
## Merge into df_1 and df_2
df_1 <- df_1 %>%
  left_join(btfp_design, by = "idrssd") %>%
  left_join(dw_design, by = "idrssd")

df_2 <- df_2 %>%
  left_join(btfp_design, by = "idrssd") %>%
  left_join(dw_design, by = "idrssd")

## Create facility design choice indicators
df_1 <- df_1 %>%
  mutate(
    high_uninsured = as.integer(uninsured_lev > median(uninsured_lev, na.rm = TRUE)),
    high_wholesale = as.integer(pct_wholesale_liability > median(pct_wholesale_liability, na.rm = TRUE)),
    funding_stability_need = high_uninsured + high_wholesale,
    collateral_constrained = as.integer(eligible_collateral < median(eligible_collateral, na.rm = TRUE)),
    low_liquidity = as.integer(pct_liquidity_available < median(pct_liquidity_available, na.rm = TRUE)),
    high_mtm = as.integer(mtm_btfp > median(mtm_btfp, na.rm = TRUE)),
    stress_indicator = high_uninsured * high_mtm,
    rate_diff = dw_avg_rate - btfp_avg_rate
  )

df_2 <- df_2 %>%
  mutate(
    high_uninsured = as.integer(uninsured_lev > median(uninsured_lev, na.rm = TRUE)),
    high_wholesale = as.integer(pct_wholesale_liability > median(pct_wholesale_liability, na.rm = TRUE)),
    funding_stability_need = high_uninsured + high_wholesale,
    collateral_constrained = as.integer(eligible_collateral < median(eligible_collateral, na.rm = TRUE)),
    low_liquidity = as.integer(pct_liquidity_available < median(pct_liquidity_available, na.rm = TRUE)),
    high_mtm = as.integer(mtm_btfp > median(mtm_btfp, na.rm = TRUE)),
    stress_indicator = high_uninsured * high_mtm,
    rate_diff = dw_avg_rate - btfp_avg_rate
  )

df <- df_1
cat("✓ Design variables merged and indicators created\n")
#> ✓ Design variables merged and indicators created

6.2 Facility Design Summary Statistics

cat("\n=== FACILITY DESIGN SUMMARY STATISTICS ===\n")
#> 
#> === FACILITY DESIGN SUMMARY STATISTICS ===
cat("\n=== PART VI: FACILITY DESIGN CHOICE MODELS ===\n")
#> 
#> === PART VI: FACILITY DESIGN CHOICE MODELS ===
# 1. AGGREGATE BTFP DESIGN VARIABLES
cat("-> Aggregating BTFP design variables...\n")
#> -> Aggregating BTFP design variables...
btfp_design <- btfp_raw %>%
  filter(btfp_loan_date >= as.Date("2023-03-13"), btfp_loan_date <= BTFP_CLOSE) %>%
  group_by(rssd_id) %>%
  summarise(
    btfp_avg_term = weighted.mean(btfp_term, btfp_loan_amount, na.rm = TRUE),
    btfp_avg_rate = weighted.mean(btfp_interest_rate, btfp_loan_amount, na.rm = TRUE),
    btfp_avg_eff_mat = weighted.mean(btfp_effective_maturity_days, btfp_loan_amount, na.rm = TRUE),
    btfp_used_full_term = as.integer(any(btfp_term >= 360, na.rm = TRUE)),
    btfp_pct_early_repay = mean(btfp_effective_maturity_days < btfp_term, na.rm = TRUE) * 100,
    btfp_n_loans = n(), # This is the variable that was missing
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

# 2. AGGREGATE DW DESIGN VARIABLES
cat("-> Aggregating DW design variables...\n")
#> -> Aggregating DW design variables...
dw_design <- dw_raw %>%
  filter(dw_loan_date >= as.Date("2022-10-01"), dw_loan_date <= DW_DATA_END) %>%
  group_by(rssd_id) %>%
  summarise(
    dw_avg_term = weighted.mean(dw_term, dw_loan_amount, na.rm = TRUE),
    dw_avg_rate = weighted.mean(dw_interest_rate, dw_loan_amount, na.rm = TRUE),
    dw_avg_eff_mat = weighted.mean(dw_effective_maturity_days, dw_loan_amount, na.rm = TRUE),
    dw_pct_omo_coll = sum(dw_omo_eligible, na.rm = TRUE) / sum(dw_total_collateral, na.rm = TRUE) * 100,
    dw_n_loans = n(),
    .groups = "drop"
  ) %>%
  rename(idrssd = rssd_id)

# 3. MERGE DESIGN VARIABLES INTO DF_1 (Re-running to ensure columns exist)
cat("-> Merging design variables into df_1...\n")
#> -> Merging design variables into df_1...
# Remove columns if they already exist to avoid .x/.y duplicates
cols_to_remove <- c(names(btfp_design)[names(btfp_design) != "idrssd"], 
                    names(dw_design)[names(dw_design) != "idrssd"])
df_1 <- df_1 %>% select(-any_of(cols_to_remove))

df_1 <- df_1 %>%
  left_join(btfp_design, by = "idrssd") %>%
  left_join(dw_design, by = "idrssd") %>%
  mutate(
    rate_diff = dw_avg_rate - btfp_avg_rate,
    # Ensure binary flags are 0 if NA (for logic checks), keep means as NA
    btfp_used_full_term = coalesce(btfp_used_full_term, 0L)
  )

# 4. GENERATE SUMMARIES
cat("\n=== FACILITY DESIGN SUMMARY STATISTICS ===\n")
#> 
#> === FACILITY DESIGN SUMMARY STATISTICS ===
## BTFP Users Summary
btfp_summary <- df_1 %>%
  filter(btfp == 1) %>%
  summarise(
    n = n(),
    avg_term = mean(btfp_avg_term, na.rm = TRUE),
    avg_rate = mean(btfp_avg_rate, na.rm = TRUE),
    avg_eff_mat = mean(btfp_avg_eff_mat, na.rm = TRUE),
    pct_full_term = mean(btfp_used_full_term == 1, na.rm = TRUE) * 100,
    avg_n_loans = mean(btfp_n_loans, na.rm = TRUE),
    pct_early_repay = mean(btfp_pct_early_repay, na.rm = TRUE)
  )

## DW Users Summary
dw_summary <- df_1 %>%
  filter(dw == 1) %>%
  summarise(
    n = n(),
    avg_term = mean(dw_avg_term, na.rm = TRUE),
    avg_rate = mean(dw_avg_rate, na.rm = TRUE),
    avg_eff_mat = mean(dw_avg_eff_mat, na.rm = TRUE),
    pct_omo_coll = mean(dw_pct_omo_coll, na.rm = TRUE),
    avg_n_loans = mean(dw_n_loans, na.rm = TRUE)
  )

## Both Users Summary
both_summary <- df_1 %>%
  filter(both == 1) %>%
  summarise(
    n = n(),
    btfp_avg_term = mean(btfp_avg_term, na.rm = TRUE),
    dw_avg_term = mean(dw_avg_term, na.rm = TRUE),
    btfp_avg_rate = mean(btfp_avg_rate, na.rm = TRUE),
    dw_avg_rate = mean(dw_avg_rate, na.rm = TRUE),
    avg_rate_diff = mean(rate_diff, na.rm = TRUE)
  )

# 5. PRINT REPORT
cat("\n[A] BTFP Users (n =", btfp_summary$n, "):\n")
#> 
#> [A] BTFP Users (n = 1259 ):
cat("  Avg Term:               ", round(btfp_summary$avg_term, 1), "days\n")
#>   Avg Term:                301.4 days
cat("  Avg Rate:               ", round(btfp_summary$avg_rate, 2), "%\n")
#>   Avg Rate:                4.99 %
cat("  Avg Effective Maturity: ", round(btfp_summary$avg_eff_mat, 1), "days\n")
#>   Avg Effective Maturity:  161.3 days
cat("  % Using Full Term:      ", round(btfp_summary$pct_full_term, 1), "%\n")
#>   % Using Full Term:       83.3 %
cat("  Avg # Loans:            ", round(btfp_summary$avg_n_loans, 1), "\n")
#>   Avg # Loans:             5.1
cat("  % Early Repayment:      ", round(btfp_summary$pct_early_repay, 1), "%\n")
#>   % Early Repayment:       67.7 %
cat("\n[B] DW Users (n =", dw_summary$n, "):\n")
#> 
#> [B] DW Users (n = 1004 ):
cat("  Avg Term:               ", round(dw_summary$avg_term, 1), "days\n")
#>   Avg Term:                5 days
cat("  Avg Rate:               ", round(dw_summary$avg_rate, 2), "%\n")
#>   Avg Rate:                5.08 %
cat("  Avg Effective Maturity: ", round(dw_summary$avg_eff_mat, 1), "days\n")
#>   Avg Effective Maturity:  3.6 days
cat("  % OMO-Eligible Coll:    ", round(dw_summary$pct_omo_coll, 1), "%\n")
#>   % OMO-Eligible Coll:     34.3 %
cat("  Avg # Loans:            ", round(dw_summary$avg_n_loans, 1), "\n")
#>   Avg # Loans:             8.9
cat("\n[C] Banks Using Both (n =", both_summary$n, "):\n")
#> 
#> [C] Banks Using Both (n = 413 ):
cat("  BTFP Avg Term:          ", round(both_summary$btfp_avg_term, 1), "days\n")
#>   BTFP Avg Term:           279.2 days
cat("  DW Avg Term:            ", round(both_summary$dw_avg_term, 1), "days\n")
#>   DW Avg Term:             6.2 days
cat("  BTFP Avg Rate:          ", round(both_summary$btfp_avg_rate, 2), "%\n")
#>   BTFP Avg Rate:           4.98 %
cat("  DW Avg Rate:            ", round(both_summary$dw_avg_rate, 2), "%\n")
#>   DW Avg Rate:             5.05 %
cat("  Avg Rate Diff (DW-BTFP):", round(both_summary$avg_rate_diff, 2), "pp\n")
#>   Avg Rate Diff (DW-BTFP): 0.07 pp

6.3 Model 1: Funding Stability Need and Term Choice

# ==========================================================================
# MODEL 1: FUNDING STABILITY NEED → TERM PREFERENCE
# ==========================================================================

cat("\n=== MODEL 1: FUNDING STABILITY AND FACILITY CHOICE ===\n")
#> 
#> === MODEL 1: FUNDING STABILITY AND FACILITY CHOICE ===
model1_btfp <- feols(
  btfp ~ funding_stability_need + mtm_btfp + eligible_collateral + ln_assets + cash_ratio,
  data = df_1, vcov = "hetero"
)

model1_dw <- feols(
  dw ~ funding_stability_need + mtm_btfp + eligible_collateral + ln_assets + cash_ratio,
  data = df_1, vcov = "hetero"
)

btfp_users_design <- df_1 %>% filter(btfp == 1, !is.na(btfp_avg_term))

model1_term <- feols(
  btfp_avg_term ~ funding_stability_need + mtm_btfp + eligible_collateral + borrowing_subsidy + ln_assets,
  data = btfp_users_design, vcov = "hetero"
)

model1_fullterm <- feols(
  btfp_used_full_term ~ funding_stability_need + mtm_btfp + eligible_collateral + borrowing_subsidy + ln_assets,
  data = btfp_users_design, vcov = "hetero"
)

modelsummary(
  list("BTFP Selection" = model1_btfp, "DW Selection" = model1_dw,
       "BTFP Term" = model1_term, "Full Term (365d)" = model1_fullterm),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "funding_stability_need" = "Funding Stability Need (0-2)",
    "mtm_btfp" = "MTM Loss (BTFP-Eligible)",
    "eligible_collateral" = "Eligible Collateral",
    "borrowing_subsidy" = "Borrowing Subsidy"
  ),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0),
                 list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 6.1: Funding Stability Need and Facility Design Choice"
)
Table 6.1: Funding Stability Need and Facility Design Choice
BTFP Selection DW Selection BTFP Term Full Term (365d)
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.357*** -0.920*** 448.750*** 1.216***
(0.069) (0.064) (40.309) (0.116)
Funding Stability Need (0-2) 0.030*** 0.022** 8.559 0.018
(0.011) (0.010) (5.600) (0.016)
MTM Loss (BTFP-Eligible) 0.060*** 0.016* 3.488 -0.001
(0.013) (0.009) (6.543) (0.019)
Eligible Collateral -0.001 -0.002** -0.073 0.001
(0.001) (0.001) (0.581) (0.002)
ln_assets 0.050*** 0.089*** -11.797*** -0.030***
(0.005) (0.005) (3.105) (0.009)
cash_ratio -0.007*** -0.001
(0.001) (0.001)
Borrowing Subsidy 0.005*** 0.000***
(0.001) (0.000)
N 4282 4282 1259 1259
0.082 0.118 0.015 0.013

6.4 Model 2: Collateral Constraints and Dual Usage

# ==========================================================================
# MODEL 2: COLLATERAL CONSTRAINTS → DUAL FACILITY USAGE
# ==========================================================================

cat("\n=== MODEL 2: COLLATERAL CONSTRAINTS AND DUAL USAGE ===\n")
#> 
#> === MODEL 2: COLLATERAL CONSTRAINTS AND DUAL USAGE ===
btfp_users <- df_1 %>% filter(btfp == 1)

model2a <- feols(
  dw ~ collateral_constrained + mtm_btfp + uninsured_lev + ln_assets + cash_ratio,
  data = btfp_users, vcov = "hetero"
)

model2b <- feols(
  dw ~ btfp_utilization + mtm_btfp + uninsured_lev + ln_assets + cash_ratio,
  data = btfp_users %>% filter(!is.na(btfp_utilization)), vcov = "hetero"
)

model2c <- feols(
  dw ~ maxed_out_btfp + mtm_btfp + uninsured_lev + ln_assets,
  data = btfp_users, vcov = "hetero"
)

modelsummary(
  list("DW | Collateral Constrained" = model2a, "DW | Utilization" = model2b, "DW | Maxed Out" = model2c),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "collateral_constrained" = "Collateral Constrained",
    "btfp_utilization" = "BTFP Utilization",
    "maxed_out_btfp" = "Maxed Out BTFP (>90%)"
  ),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0),
                 list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 6.2: Collateral Constraints and DW Usage (BTFP Users)"
)
Table 6.2: Collateral Constraints and DW Usage (BTFP Users)
DW | Collateral Constrained DW | Utilization DW | Maxed Out
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) -0.917*** -0.917*** -0.939***
(0.127) (0.125) (0.117)
Collateral Constrained 0.002
(0.031)
mtm_btfp 0.007 0.007 0.005
(0.018) (0.015) (0.015)
uninsured_lev 0.003** 0.003** 0.003**
(0.001) (0.001) (0.001)
ln_assets 0.088*** 0.087*** 0.090***
(0.010) (0.010) (0.010)
cash_ratio -0.002 -0.002
(0.002) (0.002)
BTFP Utilization 0.000
(0.000)
Maxed Out BTFP (>90%) -0.013
(0.026)
N 1259 1259 1259
0.097 0.098 0.097

6.5 Model 3: Stress and Term/Maturity Choice

# ==========================================================================
# MODEL 3: BANK STRESS → TERM AND EFFECTIVE MATURITY
# ==========================================================================

cat("\n=== MODEL 3: STRESS AND TERM/MATURITY CHOICE ===\n")
#> 
#> === MODEL 3: STRESS AND TERM/MATURITY CHOICE ===
model3_term <- feols(
  btfp_avg_term ~ stress_indicator + run_risk_1_dummy + mtm_btfp + uninsured_lev + eligible_collateral + ln_assets,
  data = btfp_users_design, vcov = "hetero"
)

model3_effmat <- feols(
  btfp_avg_eff_mat ~ stress_indicator + run_risk_1_dummy + mtm_btfp + uninsured_lev + eligible_collateral + ln_assets,
  data = btfp_users_design, vcov = "hetero"
)

model3_early <- feols(
  btfp_pct_early_repay ~ stress_indicator + run_risk_1_dummy + mtm_btfp + uninsured_lev + eligible_collateral + ln_assets,
  data = btfp_users_design, vcov = "hetero"
)

modelsummary(
  list("Avg Term" = model3_term, "Avg Eff. Maturity" = model3_effmat, "% Early Repay" = model3_early),
  stars = c('*' = 0.1, '**' = 0.05, '***' = 0.01),
  coef_rename = c(
    "stress_indicator" = "Stress (High Unins. × High MTM)",
    "run_risk_1_dummy" = "Run Risk Dummy"
  ),
  gof_map = list(list("raw" = "nobs", "clean" = "N", "fmt" = 0),
                 list("raw" = "r.squared", "clean" = "R²", "fmt" = 3)),
  title = "Table 6.3: Bank Stress and Term/Maturity Choices (BTFP Users)"
)
Table 6.3: Bank Stress and Term/Maturity Choices (BTFP Users)
Avg Term Avg Eff. Maturity % Early Repay
* p < 0.1, ** p < 0.05, *** p < 0.01
(Intercept) 434.343*** 314.439*** 96.454***
(41.427) (31.827) (11.617)
Stress (High Unins. × High MTM) 13.003 -3.240 6.479**
(10.011) (8.885) (3.031)
Run Risk Dummy 14.736 7.186 3.389
(9.295) (7.978) (2.774)
mtm_btfp -1.749 2.549 -0.933
(6.539) (5.479) (1.916)
uninsured_lev -1.252*** -0.546 -0.198
(0.459) (0.343) (0.127)
eligible_collateral 0.109 -0.199 0.081
(0.590) (0.502) (0.162)
ln_assets -8.191** -10.456*** -2.027**
(3.265) (2.430) (0.901)
N 1259 1259 1259
0.022 0.025 0.012

6.6 Model 4: Multinomial Logit for Facility Choice

# ==========================================================================
# MODEL 4: MULTINOMIAL LOGIT - FACILITY CHOICE
# ==========================================================================

cat("\n=== MODEL 4: MULTINOMIAL LOGIT FOR FACILITY CHOICE ===\n")
#> 
#> === MODEL 4: MULTINOMIAL LOGIT FOR FACILITY CHOICE ===
if (!require(nnet)) install.packages("nnet")
library(nnet)

df_mlogit <- df_1 %>%
  filter(!is.na(mtm_btfp), !is.na(uninsured_lev), !is.na(eligible_collateral),
         !is.na(ln_assets), !is.na(cash_ratio)) %>%
  mutate(facility = relevel(facility, ref = "Neither"))

mlogit_model <- multinom(
  facility ~ mtm_btfp + uninsured_lev + eligible_collateral + funding_stability_need + ln_assets + cash_ratio,
  data = df_mlogit, trace = FALSE
)

cat("\nMultinomial Logit Results (Reference: Neither):\n")
#> 
#> Multinomial Logit Results (Reference: Neither):
print(summary(mlogit_model))
#> Call:
#> multinom(formula = facility ~ mtm_btfp + uninsured_lev + eligible_collateral + 
#>     funding_stability_need + ln_assets + cash_ratio, data = df_mlogit, 
#>     trace = FALSE)
#> 
#> Coefficients:
#>           (Intercept)  mtm_btfp uninsured_lev eligible_collateral
#> BTFP Only   -4.568874 0.3136218  0.0080306032        -0.006481950
#> DW Only     -8.773128 0.1646314  0.0002652848        -0.025848569
#> Both       -10.128033 0.3297974  0.0147357934        -0.009594671
#>           funding_stability_need ln_assets   cash_ratio
#> BTFP Only             0.01800539 0.2820642 -0.060485770
#> DW Only               0.06728990 0.5783067 -0.007308258
#> Both                  0.19991405 0.6186839 -0.072490960
#> 
#> Std. Errors:
#>           (Intercept)   mtm_btfp uninsured_lev eligible_collateral
#> BTFP Only   0.4695262 0.06555722   0.004574210         0.005680018
#> DW Only     0.5076211 0.09240597   0.004959264         0.007927640
#> Both        0.5795077 0.09766161   0.005717036         0.008811688
#>           funding_stability_need  ln_assets  cash_ratio
#> BTFP Only             0.07408564 0.03744553 0.007537367
#> DW Only               0.08562735 0.03981974 0.006041057
#> Both                  0.09883437 0.04474933 0.012172308
#> 
#> Residual Deviance: 8900.338 
#> AIC: 8942.338

6.7 Facility Design Visualizations

# ==========================================================================
# FIGURE 11: TERM CHOICE BY BANK STRESS LEVEL
# ==========================================================================

term_stress_data <- df_1 %>%
  filter(btfp == 1, !is.na(btfp_avg_term)) %>%
  mutate(
    stress_level = case_when(
      stress_indicator == 1 ~ "High Stress",
      run_risk_1_dummy == 1 ~ "Moderate Stress",
      TRUE ~ "Low Stress"
    ),
    stress_level = factor(stress_level, levels = c("Low Stress", "Moderate Stress", "High Stress"))
  )

p_fig11 <- ggplot(term_stress_data, aes(x = btfp_avg_term, fill = stress_level)) +
  geom_density(alpha = 0.6) +
  geom_vline(xintercept = 365, linetype = "dashed", color = "gray30") +
  annotate("text", x = 365, y = Inf, label = "Full Term (1 Year)", vjust = 2, hjust = -0.1, size = 3.5) +
  scale_fill_manual(values = c("Low Stress" = "#91bfdb", "Moderate Stress" = "#fee08b", "High Stress" = "#d73027")) +
  labs(
    title = "Figure 11: BTFP Term Choice by Bank Stress Level",
    x = "Average Loan Term (Days)", y = "Density", fill = "Stress Level"
  ) +
  theme_pub() +
  theme(legend.position = "bottom")

print(p_fig11)

ggsave(file.path(FIG_PATH, "fig11_term_stress.png"), p_fig11, width = 12, height = 8, dpi = 300, bg = "white")
# ==========================================================================
# FIGURE 12: DW USAGE BY BTFP UTILIZATION
# ==========================================================================

util_data <- df_1 %>%
  filter(btfp == 1, !is.na(btfp_utilization)) %>%
  mutate(
    util_bin = cut(btfp_utilization, 
                   breaks = c(0, 0.25, 0.5, 0.75, 0.9, Inf),
                   labels = c("0-25%", "25-50%", "50-75%", "75-90%", ">90%"),
                   include.lowest = TRUE)
  ) %>%
  group_by(util_bin) %>%
  summarise(pct_also_dw = mean(dw, na.rm = TRUE) * 100, n = n(), .groups = "drop")

p_fig12 <- ggplot(util_data, aes(x = util_bin, y = pct_also_dw)) +
  geom_col(fill = "#2166ac", alpha = 0.8) +
  geom_text(aes(label = sprintf("%.1f%%\n(n=%d)", pct_also_dw, n)), vjust = -0.3, size = 3.5) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Figure 12: DW Usage by BTFP Collateral Utilization",
    subtitle = "Among BTFP users",
    x = "BTFP Utilization Rate", y = "% Also Using DW"
  ) +
  theme_pub()

print(p_fig12)

ggsave(file.path(FIG_PATH, "fig12_utilization_dw.png"), p_fig12, width = 10, height = 8, dpi = 300, bg = "white")
# ==========================================================================
# FIGURE 13: RATE COMPARISON (BANKS USING BOTH)
# ==========================================================================

both_users <- df_1 %>%
  filter(both == 1, !is.na(btfp_avg_rate), !is.na(dw_avg_rate))

if (nrow(both_users) > 5) {
  p_fig13 <- ggplot(both_users, aes(x = btfp_avg_rate, y = dw_avg_rate)) +
    geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray50") +
    geom_point(aes(color = rate_diff), size = 3, alpha = 0.7) +
    scale_color_gradient2(low = "#2166ac", mid = "gray80", high = "#b2182b", 
                          midpoint = 0, name = "DW - BTFP") +
    labs(
      title = "Figure 13: Interest Rate Comparison (Banks Using Both)",
      subtitle = "Points above diagonal = DW rate > BTFP rate",
      x = "BTFP Avg Rate (%)", y = "DW Avg Rate (%)"
    ) +
    theme_pub()
  
  print(p_fig13)
  ggsave(file.path(FIG_PATH, "fig13_rate_comparison.png"), p_fig13, width = 10, height = 8, dpi = 300, bg = "white")
} else {
  cat("Insufficient banks using both facilities for rate comparison.\n")
}

cat("\n")
cat(strrep("=", 80), "\n")
#> ================================================================================
cat("✓✓✓ PART VI COMPLETE: Facility Design Choice Models ✓✓✓\n")
#> ✓✓✓ PART VI COMPLETE: Facility Design Choice Models ✓✓✓
cat(strrep("=", 80), "\n")
#> ================================================================================

APPENDIX: VARIABLE DICTIONARY AND SESSION INFO

A.1 Variable Dictionary

# ==========================================================================
# VARIABLE DICTIONARY
# ==========================================================================

cat("\n=== VARIABLE DICTIONARY ===\n\n")
#> 
#> === VARIABLE DICTIONARY ===
tribble(
  ~Variable, ~Type, ~Description,
  "btfp", "Binary", "Bank borrowed from BTFP",
  "dw", "Binary", "Bank borrowed from DW",
  "mtm_btfp", "Continuous", "MTM loss on BTFP-eligible / Assets",
  "mtm_other", "Continuous", "MTM loss on non-eligible / Assets",
  "uninsured_lev", "Continuous", "Uninsured deposits / Assets",
  "eligible_collateral", "Continuous", "BTFP-eligible / Assets",
  "borrowing_subsidy", "Continuous", "MTM loss on OMO-eligible/ OMO-eligible",
  "run_risk_1", "Continuous", "%Uninsured x %MTM",
  "run_risk_1_dummy", "Binary", "Both above median",
  "idcr_1", "Continuous", "IDCR (s=0.5)",
  "idcr_2", "Continuous", "IDCR (s=1.0)",
  "insolvency_1", "Continuous", "Capital metric (s=0.5)",
  "insolvency_2", "Continuous", "Capital metric (s=1.0)",
  "adjusted_equity", "Continuous", "Equity ratio - MTM loss",
  "mtm_insolvent", "Binary", "Adjusted equity < 0"
) %>%
  kbl(caption = "Variable Dictionary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Variable Dictionary
Variable Type Description
btfp Binary Bank borrowed from BTFP
dw Binary Bank borrowed from DW
mtm_btfp Continuous MTM loss on BTFP-eligible / Assets
mtm_other Continuous MTM loss on non-eligible / Assets
uninsured_lev Continuous Uninsured deposits / Assets
eligible_collateral Continuous BTFP-eligible / Assets
borrowing_subsidy Continuous MTM loss on OMO-eligible/ OMO-eligible
run_risk_1 Continuous %Uninsured x %MTM
run_risk_1_dummy Binary Both above median
idcr_1 Continuous IDCR (s=0.5)
idcr_2 Continuous IDCR (s=1.0)
insolvency_1 Continuous Capital metric (s=0.5)
insolvency_2 Continuous Capital metric (s=1.0)
adjusted_equity Continuous Equity ratio - MTM loss
mtm_insolvent Binary Adjusted equity < 0

A.2 Data Sources

# ==========================================================================
# DATA SOURCES
# ==========================================================================

cat("\n=== DATA SOURCES ===\n\n")
#> 
#> === DATA SOURCES ===
data_sources <- tibble(
  Source = c(
    "FFIEC Call Reports",
    "BTFP Loan Data",
    "Discount Window Data",
    "CRSP Stock Data",
    "Fed H.15 Rates"
  ),
  Description = c(
    "Quarterly bank balance sheet and income statement data (2021Q4-2024Q4)",
    "Loan-level BTFP borrowing: amounts, dates, collateral, rates (Mar 2023 - Mar 2024)",
    "Loan-level DW borrowing: amounts, dates, collateral, rates (Oct 2022 - Sep 2023)",
    "Public bank identification and market data via CRSP-Call Report link",
    "Interest rate data: OIS, Federal Funds, Primary Credit Rate"
  ),
  Period = c(
    "2021Q4-2024Q4",
    "Mar 13, 2023 - Mar 11, 2024",
    "Oct 1, 2022 - Sep 30, 2023",
    "2022-2023",
    "2022-2024"
  )
)

data_sources %>%
  kbl(caption = "Appendix Table A.2: Data Sources") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Appendix Table A.2: Data Sources
Source Description Period
FFIEC Call Reports Quarterly bank balance sheet and income statement data (2021Q4-2024Q4) 2021Q4-2024Q4
BTFP Loan Data Loan-level BTFP borrowing: amounts, dates, collateral, rates (Mar 2023 - Mar 2024) Mar 13, 2023 - Mar 11, 2024
Discount Window Data Loan-level DW borrowing: amounts, dates, collateral, rates (Oct 2022 - Sep 2023) Oct 1, 2022 - Sep 30, 2023
CRSP Stock Data Public bank identification and market data via CRSP-Call Report link 2022-2023
Fed H.15 Rates Interest rate data: OIS, Federal Funds, Primary Credit Rate 2022-2024

A.3 Session Information

# ==========================================================================
# SESSION INFORMATION
# ==========================================================================

cat("\n=== SESSION INFORMATION ===\n\n")
#> 
#> === SESSION INFORMATION ===
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 
#> [2] LC_CTYPE=English_United States.utf8   
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C                          
#> [5] 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] nnet_7.3-19        forcats_1.0.0      purrr_1.0.4        tidyverse_2.0.0   
#>  [5] psych_2.5.6        moments_0.14.1     DescTools_0.99.60  modelsummary_2.4.0
#>  [9] kableExtra_1.4.0   knitr_1.50         gridExtra_2.3      patchwork_1.3.2   
#> [13] scales_1.4.0       ggthemes_5.1.0     ggplot2_3.5.2      broom_1.0.9       
#> [17] lmtest_0.9-40      zoo_1.8-13         sandwich_3.1-1     fixest_0.12.1     
#> [21] readr_2.1.5        stringr_1.5.1      lubridate_1.9.4    tibble_3.2.1      
#> [25] tidyr_1.3.1        dplyr_1.1.4        data.table_1.17.0 
#> 
#> loaded via a namespace (and not attached):
#>  [1] mnormt_2.1.1        gld_2.6.8           readxl_1.4.5       
#>  [4] rlang_1.1.1         magrittr_2.0.3      dreamerr_1.4.0     
#>  [7] e1071_1.7-16        compiler_4.3.1      systemfonts_1.2.2  
#> [10] vctrs_0.6.5         pkgconfig_2.0.3     crayon_1.5.3       
#> [13] fastmap_1.2.0       backports_1.5.0     labeling_0.4.3     
#> [16] rmarkdown_2.29      tzdb_0.5.0          haven_2.5.4        
#> [19] ragg_1.3.3          bit_4.6.0           xfun_0.52          
#> [22] cachem_1.1.0        litedown_0.7        jsonlite_2.0.0     
#> [25] tinytable_0.13.0    stringmagic_1.1.2   parallel_4.3.1     
#> [28] R6_2.6.1            bslib_0.9.0         tables_0.9.31      
#> [31] stringi_1.8.7       RColorBrewer_1.1-3  boot_1.3-28.1      
#> [34] jquerylib_0.1.4     cellranger_1.1.0    numDeriv_2016.8-1.1
#> [37] estimability_1.5.1  Rcpp_1.0.14         parameters_0.27.0  
#> [40] Matrix_1.5-4.1      timechange_0.3.0    tidyselect_1.2.1   
#> [43] rstudioapi_0.17.1   yaml_2.3.10         lattice_0.21-8     
#> [46] withr_3.0.2         bayestestR_0.16.1   coda_0.19-4.1      
#> [49] evaluate_1.0.4      proxy_0.4-27        xml2_1.3.8         
#> [52] pillar_1.11.0       checkmate_2.3.2     insight_1.3.1      
#> [55] generics_0.1.4      vroom_1.6.5         hms_1.1.3          
#> [58] rootSolve_1.8.2.4   xtable_1.8-4        class_7.3-22       
#> [61] glue_1.8.0          emmeans_1.11.2-8    lmom_3.2           
#> [64] tools_4.3.1         Exact_3.3           fs_1.6.5           
#> [67] mvtnorm_1.3-3       grid_4.3.1          datawizard_1.2.0   
#> [70] nlme_3.1-162        performance_0.15.0  Formula_1.2-5      
#> [73] cli_3.6.1           textshaping_1.0.0   fansi_1.0.6        
#> [76] expm_1.0-0          viridisLite_0.4.2   svglite_2.1.3      
#> [79] gtable_0.3.6        sass_0.4.10         digest_0.6.33      
#> [82] farver_2.1.2        htmltools_0.5.9     lifecycle_1.0.4    
#> [85] httr_1.4.7          bit64_4.6.0-1       MASS_7.3-60

A.4 Sample Construction

# ==========================================================================
# SAMPLE CONSTRUCTION FLOWCHART
# ==========================================================================

cat("\n=== SAMPLE CONSTRUCTION ===\n\n")
#> 
#> === SAMPLE CONSTRUCTION ===
sample_flow <- tibble(
  Step = 1:5,
  Description = c(
    "Banks with 2022Q4 call report data",
    "Drop if missing key variables (assets, equity, deposits)",
    "BASELINE 1: Drop failed banks (n=3: SVB, Signature, FRC)",
    "BASELINE 1: Drop GSIBs (n based on 2022Q4 classification)",
    "BASELINE 1: Require OMO-eligible > 0"
  ),
  N_Banks = c(
    nrow(call_q %>% filter(quarter == "2022Q4")),
    nrow(baseline_2),
    nrow(baseline_2 %>% filter(failed_bank == 0)),
    nrow(baseline_2 %>% filter(failed_bank == 0, gsib == 0)),
    nrow(baseline_1)
  )
)

sample_flow %>%
  kbl(caption = "Appendix Table A.3: Sample Construction") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Baseline 2 = Full sample (Step 2). Baseline 1 = Restricted sample (Step 5).")
Appendix Table A.3: Sample Construction
Step Description N_Banks
1 Banks with 2022Q4 call report data 4737
2 Drop if missing key variables (assets, equity, deposits) 4737
3 BASELINE 1: Drop failed banks (n=3: SVB, Signature, FRC) 4729
4 BASELINE 1: Drop GSIBs (n based on 2022Q4 classification) 4696
5 BASELINE 1: Require OMO-eligible > 0 4292
Note:
Baseline 2 = Full sample (Step 2). Baseline 1 = Restricted sample (Step 5).
cat("\n")
cat("Baseline 1 sample size:", nrow(df_1), "banks\n")
#> Baseline 1 sample size: 4292 banks
cat("Baseline 2 sample size:", nrow(df_2), "banks\n")
#> Baseline 2 sample size: 4737 banks
cat("\nFacility usage summary (Baseline 1):\n")
#> 
#> Facility usage summary (Baseline 1):
print(table(df_1$facility))
#> 
#> BTFP Only   DW Only      Both   Neither 
#>       846       591       413      2442
cat("\nFacility usage summary (Baseline 2):\n")
#> 
#> Facility usage summary (Baseline 2):
print(table(df_2$facility))
#> 
#> BTFP Only   DW Only      Both   Neither 
#>       881       647       435      2774