Investment styles panorama: API-only comparison

Package cre.dcf

1 Aim of this vignette

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:

All 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:

2 A style-by-style manifest

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:

# 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 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

3 Risk–return cloud: project vs equity IRR

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:

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.

4 Leverage–coverage map (initial LTV vs min-DSCR)

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:

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:

5 Covenant flags and breach counts

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)"
)
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:

6 Robustness to discounting convention

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

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"
)
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.

7 Time profile of equity cash flows

A further dimension of style differentiation concerns the time profile of leveraged equity cash flows. In the canonical presets:

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:

  1. the sum of positive equity cash flows in years strictly earlier than the horizon; and
  2. the sum of all positive equity cash flows over the horizon.
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"
)
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.

8 IRR decomposition: operations vs exit

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)"
)
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.

9 Exit-yield and rental-growth sensitivities

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.

9.1 Exit-yield shock

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"
)
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.

9.2 Rental-growth shock

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"
)
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.

10 Break-even exit yield for a target equity IRR

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)
)
Break-even exit yield to hit 10.0% equity IRR by style
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:

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:

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.

11 Distressed exit comparison under covenant breach

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:

  1. Under the bullet-debt scenario, the paths of DSCR and forward LTV are computed for each style.

  2. For a given covenant regime, the first period (t^) at which either

    • (t < {}), or
    • (LTV^{}t > LTV{})

    is interpreted as a covenant breach.

  3. 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.

  4. 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)."
  )
)
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:

In a typical calibration, core and core_plus presets exhibit:

By contrast, value_added and especially opportunistic styles tend to:

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.

12 Export for audit and replication

# 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