The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
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")| scenario | irr_equity | npv_equity | irr_project | npv_project | min_dscr | max_ltv_forward | ops_share | tv_share |
|---|---|---|---|---|---|---|---|---|
| all_equity | 0.0458885 | 3862887 | 0.0458885 | 3862887 | NA | 0.0000000 | 0.3702565 | 0.6297435 |
| debt_bullet | 0.0619978 | 6353091 | 0.0458885 | 3862887 | 5.257009 | 0.4494449 | 0.3702565 | 0.6297435 |
| debt_amort | 0.0536756 | 5350105 | 0.0458885 | 3862887 | 1.028086 | 0.4087726 | 0.3702565 | 0.6297435 |
# 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")##
## Qualitative comparison of debt structures:
cat(sprintf(
"• IRR equity : bullet = %.4f%% | amort. = %.4f%%\n",
100 * bullet$irr_equity,
100 * amort$irr_equity
))## • IRR equity : bullet = 6.1998% | amort. = 5.3676%
## • Min DSCR : bullet = 5.257 | amort. = 1.028
cat(sprintf(
"• Max LTV f. : bullet = %.3f | amort. = %.3f\n",
bullet$max_ltv_forward,
amort$max_ltv_forward
))## • Max LTV f. : bullet = 0.449 | amort. = 0.409
# 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"
)##
## Interest cover summary:
## • Min ICR : bullet = 5.257 | amort. = 5.257
## • Mean ICR : bullet = 5.517 | amort. = 15.411
## • Last-year ICR (t = 10 / 10) : bullet = 5.793 | amort. = 52.629
## • Read together with DSCR, debt yield and forward LTV.
# 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"
)##
## DSCR sign summary:
## • Bullet – min DSCR = 0.125, share of negative DSCR (interest > 0): 0.0%
## • Amort. – min DSCR = 1.028, share of negative DSCR (interest > 0): 0.0%
## • Negative values can appear in transitional years.
# 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"
)##
## Equity NPV comparison:
## • Global sum of discounted equity flows (cf_all$equity_disc): 6 353 091.2521
## • Bullet scenario equity NPV (comparison summary) : 6 353 091.2521
## • Amort. scenario equity NPV (comparison summary) : 5 350 105.3940
## • Leveraged equity NPV reported in case$leveraged : 6 353 091.2521
## • Global – bullet NPV gap : 0.0000
## • Global – amort. NPV gap : 1 002 985.8580
##
## The consolidated column `equity_disc` comes from the main merged table.
## Scenario NPVs in `comparison$summary` and `case$leveraged` come from their own
## scenario-specific equity cash-flow streams, so they should be compared, not
## forced into a single algebraic identity.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.