This vignette exploits the preset YAML files shipped in
inst/extdata to construct a compact yet expressive panorama
of four canonical commercial real-estate (CRE) investment styles:
corecore_plusvalue_addedopportunisticAll four presets are processed through the same modelling pipeline
(run_case()), under a common grammar of assumptions (DCF
engine, debt schedule, covenant logic). The vignette then extracts a
restricted set of style-defining indicators that are salient for both
investors and lenders:
The overarching objective is to document, in a transparent and reproducible way, that the presets encode the expected ordering:
To make the four profiles directly comparable, the vignette begins by constructing a compact “manifest” that records, for each style:
Under the canonical calibration, the four styles are designed to satisfy a strict risk–return and leverage–coverage hierarchy:
core --> core_plus --> value_added --> opportunistic;# Retrieve manifest
tbl_print <- styles_manifest()
# Ensure expected ordering
tbl_print <- tbl_print |>
dplyr::filter(style %in% c("core", "core_plus", "value_added", "opportunistic")) |>
dplyr::mutate(
style = factor(
style,
levels = c("core", "core_plus", "value_added", "opportunistic")
)
) |>
dplyr::arrange(style) |>
dplyr::select(
style,
irr_project,
irr_equity,
dscr_min_bul,
ltv_max_fwd,
npv_equity
)
# Defensive: stop if table empty (should never happen if helpers/tests are correct)
if (nrow(tbl_print) == 0L) {
stop("No canonical presets were found. Check inst/extdata and helper logic.")
}
# Render table
knitr::kable(
tbl_print,
digits = c(0, 0, 4, 4, 3, 3, 0),
caption = "Style presets: unlevered (project IRR) and lender-salient (equity IRR, min-DSCR, forward-LTV) indicators"
)| style | irr_project | irr_equity | dscr_min_bul | ltv_max_fwd | npv_equity |
|---|---|---|---|---|---|
| core | 0 | 0.0474 | 6.3032 | 0.339 | 2356658 |
| core_plus | 0 | 0.0738 | 4.1749 | 0.566 | 2527420 |
| value_added | 0 | 0.1491 | -0.2923 | 0.625 | 2981915 |
| opportunistic | 0 | 0.2790 | -0.7075 | 0.540 | 2979129 |
The next step positions the four styles in a simple risk–return “cloud”, with unlevered project IRR on the x-axis and levered equity IRR on the y-axis. The 45-degree line represents the locus where leverage would leave IRR unchanged. Under the canonical presets, two constraints are imposed:
core --> core_plus --> value_added --> opportunistic.These inequalities are enforced both by automated tests and by the geometry of the figure.
tbl_rr <- styles_manifest() |>
dplyr::filter(style %in% c("core", "core_plus", "value_added", "opportunistic")) |>
dplyr::mutate(
style = factor(
style,
levels = c("core", "core_plus", "value_added", "opportunistic")
),
irr_uplift = irr_equity - irr_project
) |>
dplyr::arrange(style)
if (requireNamespace("ggplot2", quietly = TRUE)) {
ggplot2::ggplot(
tbl_rr,
ggplot2::aes(x = irr_project, y = irr_equity, label = style, colour = style)
) +
ggplot2::geom_abline(slope = 1, intercept = 0, linetype = 3) +
ggplot2::geom_point(size = 3) +
ggplot2::geom_text(nudge_y = 0.002, size = 3) +
ggplot2::scale_x_continuous(labels = scales::percent_format(accuracy = 0.1)) +
ggplot2::scale_y_continuous(labels = scales::percent_format(accuracy = 0.1)) +
ggplot2::labs(
title = "Risk–return cloud (project vs equity IRR)",
x = "IRR project (unlevered)",
y = "IRR equity (levered)"
)
}In typical calibrations, core-like styles cluster in the lower-left part of the chart, with limited leverage-induced uplift. Non-core styles shift towards the north-east, with equity IRR increasingly distant from project IRR, reflecting both higher risk and higher target return.
The second diagnostic focuses on the credit profile of each style from a lender’s standpoint. For each preset, and under the bullet-debt scenario, it considers:
ltv_init,
x-axis), anddscr_min_bul, y-axis).The initial LTV reflects a structural leverage choice at signing, before any business-plan uncertainty has materialised. It measures how much debt is carried relative to the acquisition price (plus costs). By contrast, the minimum DSCR captures the deepest coverage trough induced by the business plan, that is, the weakest ratio of NOI to debt service once vacancy and capex have bitten into rents while interest remains due.
For completeness, the manifest also tracks the maximum forward LTV
(ltv_max_fwd), which summarises the worst ratio of
outstanding debt to revalued asset value under the simulated plan. This
is a conditional balance-sheet indicator, after value creation
and repricing have played out.
tbl_cov <- styles_manifest() |>
dplyr::filter(style %in% c("core", "core_plus", "value_added", "opportunistic")) |>
dplyr::mutate(
style = factor(
style,
levels = c("core", "core_plus", "value_added", "opportunistic")
)
) |>
dplyr::arrange(style) |>
dplyr::select(
style,
irr_project,
irr_equity,
dscr_min_bul,
ltv_init, # structural leverage at origination
ltv_max_fwd, # worst forward LTV under the business plan
npv_equity
)
if (requireNamespace("ggplot2", quietly = TRUE)) {
ggplot2::ggplot(
tbl_cov,
ggplot2::aes(
x = ltv_init,
y = dscr_min_bul,
label = style,
colour = style
)
) +
ggplot2::geom_hline(yintercept = 1.2, linetype = 3) + # illustrative DSCR guardrail
ggplot2::geom_vline(xintercept = 0.65, linetype = 3) + # illustrative initial-LTV guardrail
ggplot2::geom_point(size = 3) +
ggplot2::geom_text(nudge_y = 0.05, size = 3) +
ggplot2::scale_x_continuous(labels = scales::percent_format(accuracy = 0.1)) +
ggplot2::labs(
title = "Leverage–coverage map",
x = "Initial LTV (bullet)",
y = "Min DSCR (bullet)"
)
}The dashed lines illustrate generic covenant guardrails (DSCR ≈ 1.20, initial LTV ≈ 65 %). In the canonical presets:
core sits comfortably in the quadrant of low LTV and
high DSCR;core_plus moves closer to the guardrails but remains
covenant-friendly;value_added and opportunistic migrate
towards higher LTV and lower DSCR, where covenant breaches become
plausible if the business plan underperforms.Beyond static summaries, one often wishes to know how frequently a given style approaches or breaches covenant thresholds over the life of the loan. The next block explores this dimension, again under the bullet-debt scenario for comparability.
guard <- list(min_dscr = 1.20, max_ltv = 0.65)
breach_tbl <- styles_breach_counts(
styles = c("core", "core_plus", "value_added", "opportunistic"),
min_dscr_guard = guard$min_dscr,
max_ltv_guard = guard$max_ltv
)
knitr::kable(
breach_tbl,
caption = "Covenant-breach counts by style (bullet)"
)| style | n_dscr_breach | n_ltv_breach |
|---|---|---|
| core | 1 | 0 |
| core_plus | 1 | 0 |
| value_added | 3 | 0 |
| opportunistic | 2 | 0 |
Under typical assumptions:
core and core_plus exhibit zero or very
few breaches, even under relatively tight guardrails;value_added and especially opportunistic
generate more frequent or earlier breaches, concentrating credit risk in
the non-core segment.The canonical presets are first calibrated under a WACC-based
discounting convention: the discount rate combines a cost of equity, a
cost of debt and a target LTV according to the chosen
disc_method. This choice is standard in valuation practice,
but it is important to verify that the ordinal risk–return
ranking of styles does not hinge on this specific convention.
A simple robustness check consists in re-evaluating the same YAML
presets under a more parsimonious "yield_plus_growth" rule,
while leaving all cash-flow assumptions unchanged. In this alternative,
the discount rate is reconstructed as
entry_yield;
andindex_rate,without explicit reference to capital structure.
The helper styles_revalue_yield_plus_growth() performs
this re-evaluation style by style and returns the leveraged equity IRR
and NPV under the alternative convention. These can be compared with the
baseline WACC outputs from styles_manifest():
styles_vec <- c("core", "core_plus", "value_added", "opportunistic")
# Baseline (WACC) equity metrics from the manifest
base_tbl <- styles_manifest(styles_vec) |>
dplyr::select(style, irr_equity, npv_equity)
# Re-evaluation under the yield+growth rule
yg_tbl <- styles_revalue_yield_plus_growth(styles_vec)
rob_tbl <- dplyr::left_join(base_tbl, yg_tbl, by = "style") |>
dplyr::mutate(
delta_npv = npv_equity_y - npv_equity
)
knitr::kable(
rob_tbl,
digits = 4,
caption = "Robustness: equity IRR (invariant) and NPV under WACC vs yield+growth"
)| style | irr_equity | npv_equity | irr_equity_y | npv_equity_y | delta_npv |
|---|---|---|---|---|---|
| core | 0.0474 | 2356658 | 0.0474 | -2234089.7 | -4590748.2 |
| core_plus | 0.0738 | 2527420 | 0.0738 | 499308.1 | -2028112.3 |
| value_added | 0.1491 | 2981915 | 0.1491 | 1430425.6 | -1551489.8 |
| opportunistic | 0.2790 | 2979129 | 0.2790 | 2148209.2 | -830919.9 |
In the current calibration, equity IRRs are identical across the two conventions (by construction), while equity NPVs differ. The persistence of the risk–return ordering across discounting schemes indicates that the style hierarchy is driven by the cash-flow patterns and leverage choices, rather than by an arbitrary discounting recipe.
A further dimension of style differentiation concerns the time profile of leveraged equity cash flows. In the canonical presets:
core and core_plus configurations are
calibrated to return a meaningful fraction of equity progressively, on
the back of relatively stable NOI and modest refinancing risk;value_added and opportunistic strategies
tend to back-load value creation into the terminal event, with thinner
interim distributions and a stronger dependence on the exit.The helper styles_equity_cashflows() extracts, for each
style, the year-by-year equity cash flow under the leveraged scenario.
Rather than using a coarse payback-period (which, in these presets,
coincides with the final year for all styles), the vignette constructs a
more discriminating timing indicator: the share of total positive
equity distributions received before the final year.
Formally, for each style, share_early_equity is defined
as the ratio between:
styles_vec <- c("core", "core_plus", "value_added", "opportunistic")
# 1) Equity cash flows and horizons ----------------------------------------
eq_tbl <- styles_equity_cashflows(styles_vec) |>
dplyr::group_by(style) |>
dplyr::arrange(style, year)
horizon_tbl <- eq_tbl |>
dplyr::group_by(style) |>
dplyr::summarise(
horizon_years = max(year),
.groups = "drop"
)
eq_with_h <- dplyr::left_join(eq_tbl, horizon_tbl, by = "style")
# 2) Share of total positive equity CF received before the final year ------
timing_tbl <- eq_with_h |>
dplyr::group_by(style) |>
dplyr::summarise(
total_pos_equity = sum(pmax(equity_cf, 0), na.rm = TRUE),
early_pos_equity = sum(
pmax(equity_cf, 0) * (year < horizon_years),
na.rm = TRUE
),
share_early_equity = dplyr::if_else(
total_pos_equity > 0,
early_pos_equity / total_pos_equity,
NA_real_
),
.groups = "drop"
)
knitr::kable(
timing_tbl |>
dplyr::select(style, share_early_equity),
digits = 3,
caption = "Share of total positive equity distributions received before the final year"
)| style | share_early_equity |
|---|---|
| core | 0.322 |
| core_plus | 0.339 |
| opportunistic | 0.126 |
| value_added | 0.179 |
if (requireNamespace("ggplot2", quietly = TRUE)) {
eq_cum_tbl <- eq_with_h |>
dplyr::group_by(style) |>
dplyr::mutate(cum_equity = cumsum(equity_cf))
ggplot2::ggplot(
eq_cum_tbl,
ggplot2::aes(x = year, y = cum_equity, colour = style)
) +
ggplot2::geom_hline(yintercept = 0, linetype = 3) +
ggplot2::geom_line() +
ggplot2::labs(
title = "Cumulative leveraged equity cash flows by style",
x = "Year",
y = "Cumulative equity CF"
)
}In the present calibration, this metric is around 0.31–0.34 for
core and core_plus (roughly one third of
positive equity distributions received before the terminal year), and
falls to about 0.18 for value_added and 0.13 for
opportunistic. Core-like styles therefore display a more
“annuity-like” profile, with a non-trivial portion of equity repaid
through interim distributions, whereas non-core styles concentrate
equity recovery in the final transaction.
The timing indicator thus complements the risk–return and leverage–coverage diagnostics by making explicit when along the holding period each style actually returns capital to equity holders.
The styles also differ in the relative contribution of ongoing operations versus terminal value to project-level performance. In a stylised typology:
The helper styles_pv_split() approximates this
decomposition by computing, for each style, the present value
(discounted at the model’s DCF rate) of:
and by expressing these components as shares of total present value.
styles_vec <- c("core", "core_plus", "value_added", "opportunistic")
pv_tbl <- styles_pv_split(styles_vec) |>
dplyr::mutate(style = factor(style, levels = styles_vec))
knitr::kable(
pv_tbl |>
dplyr::select(style, share_pv_income, share_pv_resale),
digits = 3,
caption = "Present-value split between income and resale by style (discounted at DCF rate, t >= 1)"
)| style | share_pv_income | share_pv_resale |
|---|---|---|
| core | 0.352 | 0.648 |
| core_plus | 0.322 | 0.678 |
| value_added | 0.122 | 0.878 |
| opportunistic | 0.007 | 0.993 |
In line with the targeted calibration, core and core_plus
configurations exhibit a higher share_pv_income, signalling
that recurring NOI explains most of the present value. Value_added and
opportunistic presets display a larger share_pv_resale,
consistent with a greater dependence on exit conditions.
A different perspective on style differentiation is obtained by examining how sensitive each profile is to small shocks on exit yield and on rental growth. Strategies that rely heavily on value capture at exit should exhibit a larger change in equity IRR for a given shift in exit yield; they effectively have a longer “duration” with respect to terminal-value assumptions.
The first diagnostic perturbs the exit-yield spread by ±50 basis
points around its baseline value and recomputes leveraged equity IRR for
each style. Formally, for each preset (s) and shock (y ) bps, the helper
styles_exit_sensitivity() shifts
[ + y]
and runs run_case() under otherwise unchanged
assumptions.
## Sensitivity to +/- 50 bps on exit yield ----------------------------------
styles_vec <- c("core", "core_plus", "value_added", "opportunistic")
exit_sens <- styles_exit_sensitivity(
styles = styles_vec,
delta_bps = c(-50, 0, 50)
)
knitr::kable(
exit_sens |>
tidyr::pivot_wider(
names_from = shock_bps,
values_from = irr_equity
),
digits = 4,
caption = "Equity IRR sensitivity to +/- 50 bps exit-yield shock by style"
)| style | -50 | 0 | 50 |
|---|---|---|---|
| core | 0.0599 | 0.0474 | 0.0362 |
| core_plus | 0.0916 | 0.0738 | 0.0569 |
| value_added | 0.1691 | 0.1491 | 0.1301 |
| opportunistic | 0.3194 | 0.2790 | 0.2408 |
In the canonical calibration, core and
core_plus show relatively modest IRR changes when exit
yields move by ±50 bps, consistent with a larger share of value coming
from intermediate NOI. By contrast, value_added and
opportunistic profiles typically exhibit a stronger IRR
response to the same perturbation, reflecting their concentration of
performance in the terminal value and the greater effective duration of
their cash-flow profile.
A symmetric diagnostic focuses on the dependence of each style on
rental growth and indexation. Here the global index_rate
parameter is shifted by ±1 percentage point, and leveraged equity IRR is
recomputed for each shocked scenario.
## Sensitivity to rental-growth shocks --------------------------------------
growth_sens <- styles_growth_sensitivity(
styles = styles_vec,
delta = c(-0.01, 0, 0.01)
)
knitr::kable(
growth_sens |>
tidyr::pivot_wider(
names_from = shock_growth,
values_from = irr_equity
),
digits = 4,
caption = "Equity IRR sensitivity to rental-growth shocks by style"
)| style | -0.01 | 0 | 0.01 |
|---|---|---|---|
| core | 0.0346 | 0.0474 | 0.0599 |
| core_plus | 0.0570 | 0.0738 | 0.0897 |
| value_added | 0.1310 | 0.1491 | 0.1665 |
| opportunistic | 0.2593 | 0.2790 | 0.2984 |
This table highlights how strongly each profile depends on NOI growth to reach its target return. In line with the stylised typology, core configurations are comparatively less sensitive to a ±1 percentage-point change in indexation, as their business model rests on stabilised occupancy and moderate leverage rather than aggressive growth assumptions. Conversely, value_added and especially opportunistic strategies exhibit more pronounced IRR movements, consistent with their exposure to lease-up, reversion and rental growth embedded in the business plan.
A further synthetic indicator is the break-even exit yield required for each style to achieve a common target equity IRR. This provides an intuitive measure of how “tight” exit assumptions must be for the business plan to meet a given hurdle.
For a style (s) and a target equity IRR ({r}), the helper
styles_break_even_exit_yield() solves, via
uniroot(), for the exit yield (y^) such that
[ ^{}_s(y^) = {r},]
holding all other configuration parameters fixed. In practice, the
function reconstructs the spread exit_yield_spread_bps
implied by a candidate (y^), reruns run_case(), and
searches for the root over a bounded interval.
target_irr <- 0.10 # 10% equity IRR as illustrative hurdle
be_tbl <- styles_break_even_exit_yield(
styles = c("core", "core_plus", "value_added", "opportunistic"),
target_irr = target_irr
)
knitr::kable(
be_tbl,
digits = 4,
caption = sprintf("Break-even exit yield to hit %.1f%% equity IRR by style", 100 * target_irr)
)| style | target_irr | be_exit_yield |
|---|---|---|
| core | 0.1 | NA |
| core_plus | 0.1 | 0.0477 |
| value_added | 0.1 | 0.0784 |
| opportunistic | 0.1 | 0.0980 |
Interpreting this table requires keeping in view the baseline equity IRRs of the four presets under their unperturbed exit yields. In the current calibration, these baselines are approximately:
core: ( _e % ),core_plus: ( _e % ),value_added: ( _e % ),opportunistic: ( _e % ).A 10 % hurdle is therefore ambitious for the core and core_plus
presets, but modest for the value_added and opportunistic ones. This
asymmetry explains the pattern usually observed in
be_tbl:
core, the equity IRR never reaches 10 % within a
realistic exit-yield bracket (for example ([3%, 10%])). The
corresponding be_exit_yield is therefore NA.
Economically, this means that, at the given purchase price and leverage,
a 10 % equity IRR is simply unattainable without implausibly tight exit
pricing. This is consistent with the role of core as a low-risk,
low-return style.core_plus, the baseline IRR lies below 10 %, so the
root is found by tightening the exit yield. The reported break-even exit
yield is below the baseline yield and can be read as the level of
pricing perfection required for a core_plus deal to attain a
double-digit equity IRR.value_added and opportunistic,
baseline IRRs exceed 10 %. The root is therefore reached by
widening the exit yield (higher yield, lower price) until the
IRR falls back down to 10 %. The corresponding break-even yields are
markedly higher than the baselines, meaning that these non-core styles
can absorb a substantial deterioration in exit pricing and still deliver
10 % to equity.Under this interpretation, the break-even table does not state that non-core styles “require tighter yields” to be viable. Instead, it quantifies how much adverse repricing each style can withstand before falling below a given equity-IRR benchmark. Core exhibits almost no buffer relative to a 10 % target; core_plus has a narrow margin; value_added and opportunistic display a much larger tolerance to exit-yield widening, reflecting both their higher ex ante returns and their greater exposure to exit-pricing risk.
The same machinery used to construct baseline credit profiles can be mobilised to emulate a simplified distressed-exit mechanism. The aim is not to model a full restructuring process, but to approximate a lender-driven sale triggered when covenants are breached.
In this stylised setting, a distressed exit is defined as follows:
Under the bullet-debt scenario, the paths of DSCR and forward LTV are computed for each style.
For a given covenant regime, the first period (t^) at which either
is interpreted as a covenant breach.
If a breach occurs before a stylised refinancing window (for instance year 3), the exit is shifted to the start of that window; otherwise, the exit takes place at the breach year.
At the distressed exit date, the exit yield is penalised by a fire-sale spread (e.g. +100 bps), and the case is re-run with a shortened horizon.
Because distressed cash-flow patterns can be extreme, the equity IRR
may become undefined (no sign change in the equity cash-flow vector).
Rather than forcing an artefactual IRR, the diagnostic explicitly
preserves such NA outcomes and supplements them with more
robust performance indicators:
The helper styles_distressed_exit() (defined in the
package utilities) encapsulates this logic. The vignette uses it with
three illustrative covenant regimes:
and applies a one-percentage-point fire-sale penalty to the exit yield in all regimes.
## Distressed exit diagnostic across regimes --------------------------------
# Covenant regimes: strict / baseline / flexible
regimes <- tibble::tibble(
regime = c("strict", "baseline", "flexible"),
min_dscr = c(1.20, 1.15, 1.10),
max_ltv = c(0.65, 0.70, 0.75)
)
distress_tbl <- styles_distressed_exit(
styles = c("core", "core_plus", "value_added", "opportunistic"),
regimes = regimes,
fire_sale_bps = 100, # +100 bps exit-yield penalty
refi_min_year = 3L, # refinancing window opens in year 3
allow_year1_distress = FALSE # breaches before year 3 --> exit at year 3
)
# For compact display in the vignette, focus on the baseline regime
distress_baseline <- distress_tbl |>
dplyr::filter(regime == "baseline") |>
dplyr::select(
style,
breach_year,
breach_type,
irr_equity_base,
irr_equity_distress,
distress_undefined,
equity_multiple_base,
equity_multiple_distress,
equity_loss_pct_distress
) |>
dplyr::arrange(style)
knitr::kable(
distress_baseline,
digits = c(0, 0, 0, 4, 4, 0, 2, 2, 2),
caption = paste(
"Baseline distressed-exit diagnostic by style (bullet debt scenario,",
"+100 bps fire-sale penalty; breaches before year 3 shifted to year 3)."
)
)| style | breach_year | breach_type | irr_equity_base | irr_equity_distress | distress_undefined | equity_multiple_base | equity_multiple_distress | equity_loss_pct_distress |
|---|---|---|---|---|---|---|---|---|
| core | NA | NA | 0.0474 | NA | FALSE | 1.46 | NA | NA |
| core_plus | NA | NA | 0.0738 | NA | FALSE | 1.59 | NA | NA |
| opportunistic | 3 | dscr | 0.2790 | NA | TRUE | 2.20 | NA | NA |
| value_added | 3 | dscr | 0.1491 | NA | TRUE | 2.30 | NA | NA |
This table is read as follows:
breach_year and
breach_type locate the first covenant failure under the
baseline regime (DSCR 1.15, forward LTV 70 %).irr_equity_base reports the baseline leveraged IRR
under the standard horizon and exit yield, while
irr_equity_distress reports the IRR under the shortened,
fire-sale horizon. When the distressed cash-flow path does not contain
both negative and positive equity flows, the IRR is left undefined and
flagged by distress_undefined = TRUE.equity_multiple_base and
equity_multiple_distress summarise total equity returned
relative to equity paid in in the baseline and distressed cases,
respectively; equity_loss_pct_distress reports the loss
percentage implied by the distressed multiple.In a typical calibration, core and
core_plus presets exhibit:
core);By contrast, value_added and especially
opportunistic styles tend to:
equity_loss_pct_distress, signalling substantial or
near-total loss of the initial equity stake.This comparison operationalises, in reduced form, the idea that non-core strategies are structurally more exposed to covenant-driven forced-sale dynamics and to value capture concentrated in the terminal event. Core-like strategies, in contrast, show both delayed breaches and more resilient equity profiles, even under penalised exit conditions.
# Export results and breaches (CSV) to facilitate off-notebook auditing
out_dir <- tempfile("cre_dcf_styles_")
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
readr::write_csv(tbl_print, file.path(out_dir, "styles_summary.csv"))
readr::write_csv(breach_tbl, file.path(out_dir, "covenant_breaches.csv"))
cat(sprintf("\nArtifacts written to: %s\n", out_dir))##
## Artifacts written to: /tmp/RtmpNUZBPr/cre_dcf_styles_1944cc1ad4da49