knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE) library(cre.dcf) library(dplyr)
This vignette compares bullet and amortizing debt structures on the same stabilised operating case.
cfg_path <- system.file("extdata", "preset_core.yml", package = "cre.dcf") stopifnot(nzchar(cfg_path)) cfg <- yaml::read_yaml(cfg_path) case <- run_case(cfg) cmp <- case$comparison stopifnot(is.list(cmp), is.data.frame(cmp$summary)) # Ensure expected fields are present required_fields <- c("scenario","irr_equity","npv_equity","min_dscr","max_ltv_forward") stopifnot(all(required_fields %in% names(cmp$summary))) knitr::kable(cmp$summary, caption = "Summary comparison of bullet vs amortizing structures")
# Extract scenario rows -------------------------------------------------- rows <- split(cmp$summary, cmp$summary$scenario) stopifnot(all(c("debt_bullet", "debt_amort") %in% names(rows))) bullet <- rows$debt_bullet amort <- rows$debt_amort # readable summary -------------------------------------------- cat("\nQualitative comparison of debt structures:\n") cat(sprintf( "• IRR equity : bullet = %.4f%% | amort. = %.4f%%\n", 100 * bullet$irr_equity, 100 * amort$irr_equity )) cat(sprintf( "• Min DSCR : bullet = %.3f | amort. = %.3f\n", bullet$min_dscr, amort$min_dscr )) cat(sprintf( "• Max LTV f. : bullet = %.3f | amort. = %.3f\n", bullet$max_ltv_forward, amort$max_ltv_forward )) # Expected financial ordering (sanity checks) ---------------------------- ## (a) Leverage effect on IRR - bullet should give a higher equity IRR stopifnot(bullet$irr_equity > amort$irr_equity) ## (b) DSCR - the ordering is not universal and depends on where the NOI trough ## occurs relative to the amortization profile. Here we only check that the ## summary exposes interpretable finite values. stopifnot(is.finite(bullet$min_dscr)) stopifnot(is.finite(amort$min_dscr)) ## (c) Forward LTV - amortizing structure should deleverage over time stopifnot(bullet$max_ltv_forward > amort$max_ltv_forward)
In this stabilised core case, bullet debt keeps leverage higher for longer, while amortization improves the balance-sheet profile over time. The exact DSCR ordering remains scenario-dependent, but the current preset is calibrated so that both structures remain legible from an underwriting standpoint.
# Extract interest-cover paths ------------------------------------------ rat_bul <- case$comparison$details$debt_bullet$ratios rat_amo <- case$comparison$details$debt_amort$ratios required_ratio_fields <- c("year", "interest_cover_ratio", "interest") stopifnot(all(required_ratio_fields %in% names(rat_bul))) stopifnot(all(required_ratio_fields %in% names(rat_amo))) # Restrict to operating years (exclude t = 0) icr_bul <- rat_bul$interest_cover_ratio[rat_bul$year >= 1] icr_amo <- rat_amo$interest_cover_ratio[rat_amo$year >= 1] icr_min_bul <- min(icr_bul, na.rm = TRUE) icr_min_amo <- min(icr_amo, na.rm = TRUE) icr_mean_bul <- mean(icr_bul, na.rm = TRUE) icr_mean_amo <- mean(icr_amo, na.rm = TRUE) last_year_bul <- max(rat_bul$year[rat_bul$year >= 1]) last_year_amo <- max(rat_amo$year[rat_amo$year >= 1]) # Last-year ICR among operating years icr_last_bul <- tail(icr_bul, 1L) icr_last_amo <- tail(icr_amo, 1L) cat( "\nInterest cover summary:\n", sprintf("• Min ICR : bullet = %.3f | amort. = %.3f\n", icr_min_bul, icr_min_amo), sprintf("• Mean ICR : bullet = %.3f | amort. = %.3f\n", icr_mean_bul, icr_mean_amo), sprintf( "• Last-year ICR (t = %d / %d) : bullet = %.3f | amort. = %.3f\n", last_year_bul, last_year_amo, icr_last_bul, icr_last_amo ), " • Read together with DSCR, debt yield and forward LTV.\n" ) # Internal sanity check: ICR must be finite whenever interest > 0 and NOI > 0 -- stopifnot(all(is.finite(rat_bul$interest_cover_ratio[rat_bul$interest > 0 & rat_bul$noi > 0]))) stopifnot(all(is.finite(rat_amo$interest_cover_ratio[rat_amo$interest > 0 & rat_amo$noi > 0])))
Bullet debt tends to support equity returns, while amortization usually improves forward LTV and reduces refinance risk. In the present vignette, this trade-off is visible on a case that looks closer to a real prime-office financing memo than the former ultra-light default example.
# DSCR availability when debt service is positive and NOI is positive ----- stopifnot("dscr" %in% names(rat_bul)) stopifnot("dscr" %in% names(rat_amo)) bul_idx <- rat_bul$payment > 0 & rat_bul$noi > 0 amo_idx <- rat_amo$payment > 0 & rat_amo$noi > 0 stopifnot(all(is.finite(rat_bul$dscr[bul_idx]))) stopifnot(all(is.finite(rat_amo$dscr[amo_idx]))) # Read the sign of DSCR -------------------------------------------------- neg_share_bul <- mean(rat_bul$dscr[bul_idx] < 0, na.rm = TRUE) neg_share_amo <- mean(rat_amo$dscr[amo_idx] < 0, na.rm = TRUE) cat( "\nDSCR sign summary:\n", sprintf( "• Bullet – min DSCR = %.3f, share of negative DSCR (interest > 0): %.1f%%\n", min(rat_bul$dscr[bul_idx], na.rm = TRUE), 100 * neg_share_bul ), sprintf( "• Amort. – min DSCR = %.3f, share of negative DSCR (interest > 0): %.1f%%\n", min(rat_amo$dscr[amo_idx], na.rm = TRUE), 100 * neg_share_amo ), " • Negative values can appear in transitional years.\n" )
# Global sum of discounted equity flows in the consolidated table -------- cf_all <- case$cashflows stopifnot("equity_disc" %in% names(cf_all)) npv_equity_sum <- sum(cf_all$equity_disc, na.rm = TRUE) stopifnot(is.finite(npv_equity_sum)) # 5.2 Scenario-level equity NPVs from the comparison summary ----------------- npv_equity_bullet <- cmp$summary$npv_equity[cmp$summary$scenario == "debt_bullet"] npv_equity_amort <- cmp$summary$npv_equity[cmp$summary$scenario == "debt_amort"] stopifnot( length(npv_equity_bullet) == 1L, length(npv_equity_amort) == 1L ) # Leveraged NPV reported in the main case object ------------------------- npv_equity_lev <- case$leveraged$npv_equity stopifnot(is.finite(npv_equity_lev)) # Read the relationship between these quantities ------------------------- gap_bullet_global <- npv_equity_sum - npv_equity_bullet gap_amort_global <- npv_equity_sum - npv_equity_amort cat( "\nEquity NPV comparison:\n", sprintf( "• Global sum of discounted equity flows (cf_all$equity_disc): %s\n", formatC(npv_equity_sum, format = 'f', big.mark = " ") ), sprintf( "• Bullet scenario equity NPV (comparison summary) : %s\n", formatC(npv_equity_bullet, format = 'f', big.mark = " ") ), sprintf( "• Amort. scenario equity NPV (comparison summary) : %s\n", formatC(npv_equity_amort, format = 'f', big.mark = " ") ), sprintf( "• Leveraged equity NPV reported in case$leveraged : %s\n", formatC(npv_equity_lev, format = 'f', big.mark = " ") ), sprintf( "• Global – bullet NPV gap : %s\n", formatC(gap_bullet_global, format = 'f', big.mark = " ") ), sprintf( "• Global – amort. NPV gap : %s\n", formatC(gap_amort_global, format = 'f', big.mark = " ") ), "\n", "The consolidated column `equity_disc` comes from the main merged table.\n", "Scenario NPVs in `comparison$summary` and `case$leveraged` come from their own\n", "scenario-specific equity cash-flow streams, so they should be compared, not\n", "forced into a single algebraic identity.\n" )
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.