TD

Thorin Dugajczyk

algorithms · markets · systems

bmw_dcf_valuation.ipynb

Discounted Cash Flow Valuation of BMW AG: A Quantitative Framework

Author: Thorin Dugajczyk Date: December 2025 Subject: BMW.DE (XETRA) Classification: Consumer Cyclical — Auto Manufacturers


Abstract

This technical document presents a systematic approach to equity valuation using Discounted Cash Flow (DCF) methodology applied to Bayerische Motoren Werke AG. The analysis integrates historical financial statement data, peer group benchmarking, and capital efficiency metrics to derive an estimate of intrinsic value. Our methodology emphasizes transparency and reproducibility through computational implementation in Python.

Keywords: DCF Valuation, Free Cash Flow, ROIC, Automotive Industry, Equity Analysis

1. Introduction

1.1 Research Objective

The primary objective of this analysis is to estimate the intrinsic value of BMW AG common equity using a multi-stage Discounted Cash Flow model. This valuation framework follows established corporate finance principles as documented in Damodaran (2012) and Koller et al. (2020).

1.2 Industry Context

The European automotive sector faces a period of structural transformation driven by regulatory requirements for emissions reduction and accelerating consumer adoption of electric vehicles. BMW AG operates within the premium segment, where brand positioning and technological differentiation remain critical competitive factors.

1.3 Analytical Framework

This analysis employs a three-stage methodology:

  1. Data Collection: Extraction of financial statements and market data via standardized API interfaces
  2. Historical Analysis: Examination of profitability trends, capital allocation, and cash flow generation
  3. Peer Benchmarking: Comparative assessment against industry participants to contextualize findings

All computational procedures are implemented using documented, version-controlled code to ensure reproducibility.

In [147]:
# =============================================================================
# Environment Configuration
# =============================================================================
# Standard library imports
import pandas as pd

# Project-specific modules for data retrieval and visualization
%load_ext autoreload
%autoreload 2
from ds import data, plots

# Initialize publication-quality plotting defaults
plots.set_style()
Out[147]:
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

2. Data Sources and Sample Construction

2.1 Data Infrastructure

The analysis utilizes a modular data access layer (ds.data) that standardizes retrieval and caching of financial information. This approach ensures consistency across multiple analytical sessions and enables efficient peer group comparisons.

In [148]:
# =============================================================================
# Data Acquisition
# =============================================================================

# Define analysis universe
ticker = "BMW.DE"
peers = ["MBG.DE", "VOW3.DE", "P911.DE"]  # Mercedes-Benz, Volkswagen, Porsche
data_start_date = "2010-01-01"

# Primary Subject: BMW AG
df_prices = data.get_stock_data(ticker, start=data_start_date)
financials = data.get_company_financials(ticker)
quarterly_financials = data.get_quarterly_financials(ticker)
info = data.get_company_info(ticker)
extra_data = data.get_holders_and_recommendations(ticker)

# Peer Group
peer_data = {}
for p in peers:
    peer_data[p] = {
        "info": data.get_company_info(p),
        "financials": data.get_company_financials(p),
        "prices": data.get_stock_data(p, start=data_start_date),
    }

# Risk-Free Rate Proxy (US 10Y Treasury)
risk_free_rate = data.get_treasury_yield("^TNX", start=data_start_date)

# Summary table
summary_data = {
    "Metric": ["Company", "Sector", "Beta", "Market Cap (€B)", "Data Range"],
    "Value": [
        info.get("shortName", ticker),
        info.get("sector"),
        f"{info.get('beta'):.2f}",
        f"€{info.get('marketCap', 0) / 1e9:,.1f}B",
        f"{df_prices.index.min():%Y-%m-%d} to {df_prices.index.max():%Y-%m-%d}",
    ],
}
display(pd.DataFrame(summary_data).style.hide(axis="index").set_caption("Data Acquisition Summary"))
Out[148]:
Loading BMW.DE from cache...
Loading BMW.DE income_stmt from cache...
Loading BMW.DE balance_sheet from cache...
Loading BMW.DE cashflow from cache...
Loading BMW.DE quarterly income_stmt from cache...
Loading BMW.DE quarterly balance_sheet from cache...
Loading BMW.DE quarterly cashflow from cache...
Loading BMW.DE info from cache...
Loading BMW.DE institutional_holders from cache...
Loading BMW.DE major_holders from cache...
Loading BMW.DE recommendations from cache...
Loading MBG.DE info from cache...
Loading MBG.DE income_stmt from cache...
Loading MBG.DE balance_sheet from cache...
Loading MBG.DE cashflow from cache...
Loading MBG.DE from cache...
Loading VOW3.DE info from cache...
Loading VOW3.DE income_stmt from cache...
Loading VOW3.DE balance_sheet from cache...
Loading VOW3.DE cashflow from cache...
Loading VOW3.DE from cache...
Loading P911.DE info from cache...
Loading P911.DE income_stmt from cache...
Loading P911.DE balance_sheet from cache...
Loading P911.DE cashflow from cache...
Loading P911.DE from cache...
Loading ^TNX from cache...
Loading BMW.DE quarterly cashflow from cache...
Loading BMW.DE info from cache...
Loading BMW.DE institutional_holders from cache...
Loading BMW.DE major_holders from cache...
Loading BMW.DE recommendations from cache...
Loading MBG.DE info from cache...
Loading MBG.DE income_stmt from cache...
Loading MBG.DE balance_sheet from cache...
Loading MBG.DE cashflow from cache...
Loading MBG.DE from cache...
Loading VOW3.DE info from cache...
Loading VOW3.DE income_stmt from cache...
Loading VOW3.DE balance_sheet from cache...
Loading VOW3.DE cashflow from cache...
Loading VOW3.DE from cache...
Loading P911.DE info from cache...
Loading P911.DE income_stmt from cache...
Loading P911.DE balance_sheet from cache...
Loading P911.DE cashflow from cache...
Loading P911.DE from cache...
Loading ^TNX from cache...
Data Acquisition Summary
Metric Value
Company BAYERISCHE MOTOREN WERKE AG S
Sector Consumer Cyclical
Beta 0.77
Market Cap (€B) €51.9B
Data Range 2019-01-02 to 2025-11-21

2.2 Peer Group Selection

The comparative analysis employs a peer group of German premium automotive manufacturers. This selection criteria ensures consistency in:

  • Regulatory Environment: EU emissions standards and labor regulations
  • Currency Exposure: Euro-denominated operations and reporting
  • Market Positioning: Premium and luxury segment competition
Ticker Company Segment
MBG.DE Mercedes-Benz Group AG Premium full-line
VOW3.DE Volkswagen AG Mass-market with premium subsidiaries
P911.DE Porsche AG Luxury/Performance

2.3 Data Elements

The following data elements are extracted for the trailing five-year period:

  1. Market Data: Daily Open-High-Low-Close-Volume (OHLCV) prices
  2. Income Statement: Revenue, EBIT, Tax Provision, Net Income
  3. Balance Sheet: Total Equity, Total Debt, Cash and Equivalents
  4. Cash Flow Statement: Operating Cash Flow, Capital Expenditures
  5. Reference Rate: US 10-Year Treasury Yield as risk-free rate proxy

3. Methodology: Financial Metrics Computation

3.1 Key Performance Indicators

The valuation framework relies on several derived metrics computed from raw financial statement data:

Metric Formula Interpretation
Free Cash Flow (FCF) OCF − CapEx Cash available to capital providers
NOPAT EBIT × (1 − Tax Rate) Operating profit after tax adjustment
Invested Capital Equity + Debt − Cash Capital deployed in operations
ROIC NOPAT / Invested Capital Return on capital employed
EBIT Margin EBIT / Revenue Operating profitability

3.2 Implementation

The following code block transforms raw financial statement data into the standardized metrics defined above. Column name matching is handled flexibly to accommodate variations in reporting conventions.

In [149]:
# =============================================================================
# Financial Metrics Computation
# =============================================================================


def get_col(df: pd.DataFrame, keywords: list[str]) -> pd.Series:
    """Retrieve a column from a DataFrame by matching partial keywords."""
    for col in df.columns:
        if any(k in col for k in keywords):
            return df[col]
    return pd.Series(0, index=df.index)


def prepare_financial_history(financials_dict: dict[str, pd.DataFrame]) -> pd.DataFrame:
    """Transform raw financial statements into standardized analytical metrics."""
    income_T = financials_dict["income_stmt"].T
    balance_T = financials_dict["balance_sheet"].T
    cashflow_T = financials_dict["cashflow"].T

    df_hist = pd.DataFrame(index=income_T.index)

    # Income Statement Items
    df_hist["Revenue"] = get_col(income_T, ["Total Revenue", "Operating Revenue"])
    df_hist["EBIT"] = get_col(income_T, ["EBIT", "Operating Income"])
    df_hist["Pretax Income"] = get_col(income_T, ["Pretax Income"])
    df_hist["Tax Provision"] = get_col(income_T, ["Tax Provision", "Income Tax Expense"])
    df_hist["NetIncome"] = get_col(income_T, ["Net Income", "Net Income Common Stockholders"])

    # Balance Sheet Items
    df_hist["Total Equity"] = get_col(
        balance_T, ["Total Stockholder Equity", "Total Equity Gross Minority Interest"]
    )
    df_hist["Total Debt"] = get_col(balance_T, ["Total Debt"])
    df_hist["Cash"] = get_col(balance_T, ["Cash And Cash Equivalents"])

    # Cash Flow Items
    df_hist["OCF"] = get_col(
        cashflow_T, ["Operating Cash Flow", "Total Cash From Operating Activities"]
    )
    df_hist["CapEx"] = get_col(cashflow_T, ["Capital Expenditure"])

    # Derived Metrics
    if df_hist["CapEx"].mean() > 0:
        df_hist["FCF"] = df_hist["OCF"] - df_hist["CapEx"]
    else:
        df_hist["FCF"] = df_hist["OCF"] + df_hist["CapEx"]

    df_hist["Tax Rate"] = (df_hist["Tax Provision"] / df_hist["Pretax Income"]).fillna(0.25)
    df_hist["NOPAT"] = df_hist["EBIT"] * (1 - df_hist["Tax Rate"])
    df_hist["Invested Capital"] = df_hist["Total Equity"] + df_hist["Total Debt"] - df_hist["Cash"]
    df_hist["ROIC"] = df_hist["NOPAT"] / df_hist["Invested Capital"]
    df_hist["EBIT Margin"] = df_hist["EBIT"] / df_hist["Revenue"]
    df_hist["FCF Margin"] = df_hist["FCF"] / df_hist["Revenue"]

    return df_hist.sort_index()


# Apply Transformations
df_annual = prepare_financial_history(financials)
df_quarterly = prepare_financial_history(quarterly_financials)

peer_metrics = {}
for peer_ticker, peer_fin_data in peer_data.items():
    peer_metrics[peer_ticker] = prepare_financial_history(peer_fin_data["financials"])

# Display Summary Table
display(
    df_annual[["Revenue", "EBIT", "NetIncome", "FCF", "ROIC"]]
    .tail()
    .style.format(
        {
            "Revenue": "€{:,.0f}",
            "EBIT": "€{:,.0f}",
            "NetIncome": "€{:,.0f}",
            "FCF": "€{:,.0f}",
            "ROIC": "{:.1%}",
        }
    )
    .set_caption("Table 1: BMW AG Annual Financial Summary")
)
Out[149]:
Table 1: BMW AG Annual Financial Summary
  Revenue EBIT NetIncome FCF ROIC
2020-12-31 00:00:00 €nan €nan €nan €nan nan%
2021-12-31 00:00:00 €111,239,000,000 €22,287,000,000 €12,382,000,000 €9,295,000,000 12.1%
2022-12-31 00:00:00 €142,610,000,000 €31,362,000,000 €17,941,000,000 €14,473,000,000 17.0%
2023-12-31 00:00:00 €155,498,000,000 €27,963,000,000 €11,290,000,000 €6,661,000,000 13.5%
2024-12-31 00:00:00 €142,380,000,000 €20,783,000,000 €7,290,000,000 €-4,639,000,000 9.0%
In [150]:
# =============================================================================
# Data Preparation for Visualization
# =============================================================================
# Convert index to DatetimeIndex and extract fiscal years for plotting
df_annual.index = pd.to_datetime(df_annual.index)
years = pd.DatetimeIndex(df_annual.index).year

4. Results: Historical Performance Analysis

4.1 Dashboard Overview

Figure 1 presents a multi-panel visualization of key financial metrics. The dashboard structure facilitates comparison across dimensions:

Panel Metric Purpose
(1,1) Revenue & EBIT Margin Scale and operating leverage relationship
(1,2) FCF Composition Cash generation capacity vs. reinvestment
(2,1) Quarterly Revenue Seasonality and trend identification
(2,2) Stock Price Market valuation trajectory
(3,1) ROIC Capital efficiency over time
(3,2) Peer Comparison Relative operating performance
In [151]:
# =============================================================================
# Figure 1: Financial Performance Dashboard
# =============================================================================

fig = plots.financial_performance_dashboard(
    df_annual,
    df_quarterly,
    df_prices,
    peer_metrics,
    ticker
)
fig.show()
Out[151]:

5. DCF Valuation Model

5.1 Weighted Average Cost of Capital (WACC)

The discount rate for BMW's cash flows is computed using the Weighted Average Cost of Capital:

Cost of Equity (CAPM):

Assumed Values (Balanced)

Parameter Value Rationale
Equity Risk Premium 5.5% Industry standard for European equities
Pre-tax Cost of Debt 4.5% BMW investment-grade credit rating (A/A2); current corporate bond yields
Tax Rate 30% German statutory (15%) + trade tax (14%)
Target Debt Weight 30% Target capital structure for industrial operations; excludes captive financing
Beta 1.15 Adjusted for cyclical auto + EV transition risk (higher than reported 0.77)

Data from Source

Parameter Source
Market Cap, Debt, Cash Latest annual financial statements
Reported Beta Yahoo Finance company info
Risk-Free Rate 10Y Treasury yield (^TNX), most recent

⚠️ Important: Captive Finance Adjustment

BMW's balance sheet includes ~€100B+ of debt from BMW Financial Services (captive finance arm), which funds customer loans and leases. For an industrial operations valuation, we use adjusted net debt that excludes this self-funding financing business. Industry practice estimates BMW's industrial net debt at approximately €40–55B.

In [152]:
# =============================================================================
# WACC Calculation (Balanced Assumptions)
# =============================================================================

# Risk-Free Rate
rf = risk_free_rate.iloc[-1] / 100

# Beta (adjusted for cyclical auto + EV transition risk)
reported_beta = info.get("beta", 1.0)
beta = 1.15  # Above reported 0.77 to reflect EV transition + cyclical risk

# Equity Risk Premium (industry standard for European equities)
equity_risk_premium = 0.055  # 5.5% standard ERP

# Cost of Equity (CAPM)
cost_of_equity = rf + beta * equity_risk_premium

# Cost of Debt
cost_of_debt_pretax = 0.045
tax_rate = 0.30
cost_of_debt_aftertax = cost_of_debt_pretax * (1 - tax_rate)

# Capital Structure (reported - includes captive finance)
market_cap = info.get("marketCap", 0)
total_debt_reported = df_annual["Total Debt"].iloc[-1]
cash = df_annual["Cash"].iloc[-1]

# -----------------------------------------------------------------------------
# IMPORTANT: Captive Finance Adjustment
# -----------------------------------------------------------------------------
# BMW Financial Services debt (~€100B+) funds customer loans/leases and should be
# excluded from industrial valuation. Industry estimates BMW's industrial net debt
# at approximately €40-55B after excluding financial services.
#
# Reference: BMW Group annual report segments industrial vs. financial services.
# Industrial net debt = Automotive segment debt - cash
INDUSTRIAL_NET_DEBT = 50_000_000_000  # €50B - balanced estimate

net_debt = INDUSTRIAL_NET_DEBT  # Use adjusted industrial net debt
enterprise_value = market_cap + net_debt

# Target Capital Structure (industrial operations)
TARGET_DEBT_TO_EV = 0.30
weight_equity = 1 - TARGET_DEBT_TO_EV
weight_debt = TARGET_DEBT_TO_EV

# WACC Calculation
wacc = weight_equity * cost_of_equity + weight_debt * cost_of_debt_aftertax

# Store for later use
dcf_params = {
    "rf": rf,
    "beta": beta,
    "equity_risk_premium": equity_risk_premium,
    "cost_of_equity": cost_of_equity,
    "cost_of_debt_pretax": cost_of_debt_pretax,
    "cost_of_debt_aftertax": cost_of_debt_aftertax,
    "tax_rate": tax_rate,
    "market_cap": market_cap,
    "total_debt_reported": total_debt_reported,
    "cash": cash,
    "net_debt": net_debt,  # Industrial net debt (adjusted)
    "enterprise_value": enterprise_value,
    "weight_equity": weight_equity,
    "weight_debt": weight_debt,
    "wacc": wacc,
}

# Display WACC Summary Table
wacc_data = pd.DataFrame(
    {
        "Component": [
            "Risk-Free Rate",
            "Beta (adjusted)",
            "Equity Risk Premium",
            "Cost of Equity",
            "Cost of Debt (after-tax)",
            "Weight Equity",
            "Weight Debt",
            "WACC",
            "Industrial Net Debt (adj.)",
        ],
        "Value": [
            f"{rf:.2%}",
            f"{beta:.2f}",
            f"{equity_risk_premium:.2%}",
            f"{cost_of_equity:.2%}",
            f"{cost_of_debt_aftertax:.2%}",
            f"{weight_equity:.1%}",
            f"{weight_debt:.1%}",
            f"{wacc:.2%}",
            f"€{net_debt / 1e9:.1f}B",
        ],
    }
)
display(wacc_data.style.hide(axis="index").set_caption("Table 2: WACC Components"))
Out[152]:
Table 2: WACC Components
Component Value
Risk-Free Rate 4.06%
Beta (adjusted) 1.15
Equity Risk Premium 5.50%
Cost of Equity 10.39%
Cost of Debt (after-tax) 3.15%
Weight Equity 70.0%
Weight Debt 30.0%
WACC 8.22%
Industrial Net Debt (adj.) €50.0B
In [153]:
# =============================================================================
# WACC Component Visualization
# =============================================================================

fig = plots.wacc_analysis_chart(
    market_cap,
    net_debt,
    cost_of_equity,
    cost_of_debt_aftertax,
    wacc,
    ticker
)
fig.show()

# Summary Table
print("\nTable 2: WACC Summary")
print("-" * 50)
wacc_summary = pd.DataFrame(
    {
        "Parameter": [
            "Risk-Free Rate (Rf)",
            "Beta (β)",
            "Equity Risk Premium",
            "Cost of Equity (r_e)",
            "Cost of Debt (pre-tax)",
            "Tax Rate",
            "Cost of Debt (after-tax)",
            "Weight of Equity",
            "Weight of Debt",
            "WACC",
        ],
        "Value": [
            f"{rf:.2%}",
            f"{beta:.2f}",
            f"{equity_risk_premium:.2%}",
            f"{cost_of_equity:.2%}",
            f"{cost_of_debt_pretax:.2%}",
            f"{tax_rate:.0%}",
            f"{cost_of_debt_aftertax:.2%}",
            f"{weight_equity:.1%}",
            f"{weight_debt:.1%}",
            f"{wacc:.2%}",
        ],
    }
)
display(wacc_summary.style.hide(axis="index"))
Out[153]:

Table 2: WACC Summary
--------------------------------------------------
Parameter Value
Risk-Free Rate (Rf) 4.06%
Beta (β) 1.15
Equity Risk Premium 5.50%
Cost of Equity (r_e) 10.39%
Cost of Debt (pre-tax) 4.50%
Tax Rate 30%
Cost of Debt (after-tax) 3.15%
Weight of Equity 70.0%
Weight of Debt 30.0%
WACC 8.22%

5.2 Five-Year Free Cash Flow Forecast (2025–2029)

The explicit forecast period projects BMW's financial performance using balanced assumptions that reflect EV transition challenges while acknowledging BMW's premium positioning.

Assumed Values

Parameter Y1 Y2 Y3 Y4 Y5 Rationale
Revenue Growth 1% 1.5% 1.5% 2% 2% Modest growth; EV price competition
EBIT Margin 9% 9% 8.5% 8.5% 8.5% Margin compression from current ~14%
CapEx/Revenue 8.5% 8.5% 8% 8% 7.5% Elevated CapEx for Neue Klasse
D&A/Revenue 6% 6.5% 6.5% 7% 7% D&A rises with EV asset base
ΔNWC/ΔRevenue 12% 12% 12% 12% 12% Slightly elevated working capital
Tax Rate 30% German statutory rate

Free Cash Flow Formula:

In [154]:
# =============================================================================
# 5-Year Free Cash Flow Projection (2025-2029)
# =============================================================================

# Base Year Data
base_year = df_annual.index[-1].year
base_revenue = df_annual["Revenue"].iloc[-1]
base_ebit_margin = df_annual["EBIT Margin"].iloc[-1]

# Forecast Assumptions (balanced)
forecast_years = [base_year + i for i in range(1, 6)]
revenue_growth = [0.01, 0.015, 0.015, 0.02, 0.02]  # Modest growth
ebit_margin_forecast = [0.09, 0.09, 0.085, 0.085, 0.085]  # 8.5-9% (moderate compression from current ~14%)
capex_pct = [0.085, 0.085, 0.08, 0.08, 0.075]  # Elevated CapEx for EV transition
da_pct = [0.06, 0.065, 0.065, 0.07, 0.07]  # D&A rises with EV assets
nwc_pct_of_delta_rev = 0.12  # Slightly elevated WC needs
forecast_tax_rate = tax_rate

# Build Forecast Model
forecast_data = []
prev_revenue = base_revenue

for i, year in enumerate(forecast_years):
    revenue = prev_revenue * (1 + revenue_growth[i])
    delta_revenue = revenue - prev_revenue
    ebit = revenue * ebit_margin_forecast[i]
    nopat = ebit * (1 - forecast_tax_rate)
    da = revenue * da_pct[i]
    capex = revenue * capex_pct[i]
    delta_nwc = delta_revenue * nwc_pct_of_delta_rev
    fcf = nopat + da - capex - delta_nwc

    forecast_data.append(
        {
            "Year": year,
            "Revenue": revenue,
            "Revenue Growth": revenue_growth[i],
            "EBIT": ebit,
            "EBIT Margin": ebit_margin_forecast[i],
            "NOPAT": nopat,
            "D&A": da,
            "CapEx": capex,
            "ΔNWC": delta_nwc,
            "FCF": fcf,
            "FCF Margin": fcf / revenue,
        }
    )
    prev_revenue = revenue

df_forecast = pd.DataFrame(forecast_data).set_index("Year")

# =============================================================================
# Visualize Forecast Assumptions
# =============================================================================
fig = plots.forecast_assumptions_chart(
    forecast_years,
    revenue_growth,
    ebit_margin_forecast,
    capex_pct,
    da_pct,
    ticker
)
fig.show()

# Store for later use
dcf_params["df_forecast"] = df_forecast
dcf_params["base_revenue"] = base_revenue
dcf_params["terminal_fcf"] = df_forecast["FCF"].iloc[-1]
Out[154]:
In [155]:
# =============================================================================
# Figure 3: FCF Forecast Visualization
# =============================================================================

fig = plots.fcf_forecast_chart(
    df_annual,
    df_forecast,
    ticker,
    show_historical_years=3
)
fig.show()

# -----------------------------------------------------------------------------
# FCF Bridge Chart
# -----------------------------------------------------------------------------
print("\nTable 3: FCF Component Bridge (Terminal Year 2029)")
print("-" * 60)

terminal_year = df_forecast.iloc[-1]
bridge_data = pd.DataFrame(
    {
        "Component": ["NOPAT", "+ D&A", "− CapEx", "− ΔNWC", "= FCF"],
        "Amount (€B)": [
            terminal_year["NOPAT"] / 1e9,
            terminal_year["D&A"] / 1e9,
            -terminal_year["CapEx"] / 1e9,
            -terminal_year["ΔNWC"] / 1e9,
            terminal_year["FCF"] / 1e9,
        ],
    }
)
display(bridge_data.style.format({"Amount (€B)": "{:,.2f}"}).hide(axis="index"))
Out[155]:

Table 3: FCF Component Bridge (Terminal Year 2029)
------------------------------------------------------------
Component Amount (€B)
NOPAT 9.17
+ D&A 10.79
− CapEx -11.56
− ΔNWC -0.36
= FCF 8.04

5.3 Terminal Value

Beyond the explicit forecast period, we estimate BMW's continuing value using the Gordon Growth Model (perpetuity growth method):

Where:

  • = Terminal year free cash flow
  • = Perpetual growth rate (1.5%, long-run nominal GDP proxy)
  • = Weighted average cost of capital (~8.2%)

Cross-Check: The implied exit multiple should fall within reasonable industry ranges:

  • EV/EBITDA: 5–8× for mature automakers in EV transition
  • EV/FCF: 15–25× for capital-intensive businesses
In [156]:
# =============================================================================
# Terminal Value Calculation
# =============================================================================

# -----------------------------------------------------------------------------
# Terminal Value Assumptions
# -----------------------------------------------------------------------------
terminal_growth_rate = 0.015  # 1.5% perpetual growth (long-run nominal GDP proxy)

# Terminal year FCF
terminal_fcf = df_forecast["FCF"].iloc[-1]
terminal_year_num = df_forecast.index[-1]

print("=" * 60)
print("TERMINAL VALUE CALCULATION")
print("=" * 60)
print(f"\nTerminal Year: {terminal_year_num}")
print(f"Terminal FCF: €{terminal_fcf / 1e9:,.2f}B")
print(f"Perpetual Growth Rate (g): {terminal_growth_rate:.2%}")
print(f"WACC: {wacc:.2%}")

# -----------------------------------------------------------------------------
# Gordon Growth Model: TV = FCF × (1+g) / (WACC - g)
# -----------------------------------------------------------------------------
terminal_value = (terminal_fcf * (1 + terminal_growth_rate)) / (wacc - terminal_growth_rate)

print("\nTerminal Value Formula:")
print("  TV = FCF₂₀₂₉ × (1 + g) / (WACC - g)")
print(
    f"  TV = €{terminal_fcf / 1e9:,.2f}B × (1 + {terminal_growth_rate:.2%}) / ({wacc:.2%} - {terminal_growth_rate:.2%})"
)
print(
    f"  TV = €{terminal_fcf * (1 + terminal_growth_rate) / 1e9:,.2f}B / {wacc - terminal_growth_rate:.2%}"
)
print(f"\n  Terminal Value = €{terminal_value / 1e9:,.1f}B")

# -----------------------------------------------------------------------------
# Implied Exit Multiples (Sanity Check)
# -----------------------------------------------------------------------------
terminal_ebitda = df_forecast["EBIT"].iloc[-1] + df_forecast["D&A"].iloc[-1]
implied_ev_ebitda = terminal_value / terminal_ebitda
implied_ev_fcf = terminal_value / terminal_fcf

print("\nImplied Exit Multiples (Sanity Check):")
print(f"  Terminal EBITDA: €{terminal_ebitda / 1e9:,.2f}B")
print(f"  Implied EV/EBITDA: {implied_ev_ebitda:.1f}×")
print(f"  Implied EV/FCF: {implied_ev_fcf:.1f}×")

if 5 <= implied_ev_ebitda <= 10:
    print("  ✓ EV/EBITDA within reasonable range (5-10×)")
else:
    print("  ⚠ EV/EBITDA outside typical range (5-10×)")

# Store for later use
dcf_params["terminal_growth_rate"] = terminal_growth_rate
dcf_params["terminal_value"] = terminal_value
dcf_params["implied_ev_ebitda"] = implied_ev_ebitda
Out[156]:
============================================================
TERMINAL VALUE CALCULATION
============================================================

Terminal Year: 2029
Terminal FCF: €8.04B
Perpetual Growth Rate (g): 1.50%
WACC: 8.22%

Terminal Value Formula:
  TV = FCF₂₀₂₉ × (1 + g) / (WACC - g)
  TV = €8.04B × (1 + 1.50%) / (8.22% - 1.50%)
  TV = €8.16B / 6.72%

  Terminal Value = €121.5B

Implied Exit Multiples (Sanity Check):
  Terminal EBITDA: €23.89B
  Implied EV/EBITDA: 5.1×
  Implied EV/FCF: 15.1×
  ✓ EV/EBITDA within reasonable range (5-10×)

5.4 Present Value & Enterprise Value

The DCF valuation discounts all future cash flows back to present value:

Where:

  • First term = Present value of explicit forecast period FCFs
  • Second term = Present value of terminal value

The equity value bridge then derives shareholder value:

In [157]:
# =============================================================================
# Present Value & Enterprise Value Calculation
# =============================================================================

# -----------------------------------------------------------------------------
# Discount Factors
# -----------------------------------------------------------------------------
discount_factors = [(1 + wacc) ** t for t in range(1, 6)]

print("=" * 70)
print("DCF VALUATION: PRESENT VALUE CALCULATION")
print("=" * 70)

# -----------------------------------------------------------------------------
# Present Value of Explicit Period FCFs
# -----------------------------------------------------------------------------
pv_fcfs = []
print("\nPresent Value of Forecast Period FCFs:")
print("-" * 70)
print(f"{'Year':<8} {'FCF (€B)':<12} {'Discount Factor':<18} {'PV (€B)':<12}")
print("-" * 70)

for i, (year, row) in enumerate(df_forecast.iterrows()):
    fcf_t = row["FCF"]
    df_t = discount_factors[i]
    pv_t = fcf_t / df_t
    pv_fcfs.append(pv_t)
    print(f"{year:<8} {fcf_t / 1e9:>10,.2f}   {df_t:>16,.4f}   {pv_t / 1e9:>10,.2f}")

pv_forecast_fcfs = sum(pv_fcfs)
print("-" * 70)
print(f"{'Total PV of FCFs':<40} {pv_forecast_fcfs / 1e9:>10,.2f}")

# -----------------------------------------------------------------------------
# Present Value of Terminal Value
# -----------------------------------------------------------------------------
pv_terminal_value = terminal_value / discount_factors[-1]

print("\nPresent Value of Terminal Value:")
print(f"  TV (end of {terminal_year_num}): €{terminal_value / 1e9:,.1f}B")
print(f"  Discount Factor (Year 5): {discount_factors[-1]:.4f}")
print(f"  PV of TV: €{pv_terminal_value / 1e9:,.1f}B")

# -----------------------------------------------------------------------------
# Enterprise Value
# -----------------------------------------------------------------------------
enterprise_value_dcf = pv_forecast_fcfs + pv_terminal_value

print("\n" + "=" * 70)
print("ENTERPRISE VALUE")
print("=" * 70)
print(f"  PV of Forecast FCFs:    €{pv_forecast_fcfs / 1e9:>10,.1f}B")
print(f"  PV of Terminal Value:   €{pv_terminal_value / 1e9:>10,.1f}B")
print("  ─────────────────────────────────────")
print(f"  Enterprise Value:       €{enterprise_value_dcf / 1e9:>10,.1f}B")

# TV as % of EV (sanity check)
tv_pct_of_ev = pv_terminal_value / enterprise_value_dcf
print(f"\n  Terminal Value as % of EV: {tv_pct_of_ev:.1%}")
if tv_pct_of_ev > 0.8:
    print("  ⚠ High TV concentration - valuation sensitive to terminal assumptions")

# Store for later
dcf_params["pv_forecast_fcfs"] = pv_forecast_fcfs
dcf_params["pv_terminal_value"] = pv_terminal_value
dcf_params["enterprise_value_dcf"] = enterprise_value_dcf
Out[157]:
======================================================================
DCF VALUATION: PRESENT VALUE CALCULATION
======================================================================

Present Value of Forecast Period FCFs:
----------------------------------------------------------------------
Year     FCF (€B)     Discount Factor    PV (€B)     
----------------------------------------------------------------------
2025           5.29             1.0822         4.89
2026           6.02             1.1711         5.14
2027           6.33             1.2673         4.99
2028           7.12             1.3714         5.19
2029           8.04             1.4841         5.42
----------------------------------------------------------------------
Total PV of FCFs                              25.64

Present Value of Terminal Value:
  TV (end of 2029): €121.5B
  Discount Factor (Year 5): 1.4841
  PV of TV: €81.8B

======================================================================
ENTERPRISE VALUE
======================================================================
  PV of Forecast FCFs:    €      25.6B
  PV of Terminal Value:   €      81.8B
  ─────────────────────────────────────
  Enterprise Value:       €     107.5B

  Terminal Value as % of EV: 76.1%
In [158]:
# =============================================================================
# Equity Value Bridge & Intrinsic Value
# =============================================================================

# -----------------------------------------------------------------------------
# Net Debt (from latest balance sheet)
# -----------------------------------------------------------------------------
net_debt_valuation = dcf_params["net_debt"]

# -----------------------------------------------------------------------------
# Shares Outstanding
# -----------------------------------------------------------------------------
shares_outstanding = info.get("sharesOutstanding", 600_000_000)

# -----------------------------------------------------------------------------
# Equity Value & Intrinsic Value per Share
# -----------------------------------------------------------------------------
equity_value = enterprise_value_dcf - net_debt_valuation
intrinsic_value_per_share = equity_value / shares_outstanding

# Current market price for comparison
current_price = df_prices["Close"].iloc[-1]

# Premium / Discount
premium_discount = (current_price - intrinsic_value_per_share) / intrinsic_value_per_share

print("\n" + "=" * 70)
print("EQUITY VALUE BRIDGE")
print("=" * 70)
print(f"  Enterprise Value (DCF):  €{enterprise_value_dcf / 1e9:>10,.1f}B")
print(f"  Less: Net Debt:          €{net_debt_valuation / 1e9:>10,.1f}B")
print("  ─────────────────────────────────────")
print(f"  Equity Value:            €{equity_value / 1e9:>10,.1f}B")

print(f"\n  Shares Outstanding:      {shares_outstanding / 1e6:,.0f}M")
print("  ─────────────────────────────────────")
print(f"  Intrinsic Value/Share:   €{intrinsic_value_per_share:>10,.2f}")

print("\n" + "=" * 70)
print("VALUATION SUMMARY")
print("=" * 70)
print(f"\n  DCF Intrinsic Value:     €{intrinsic_value_per_share:,.2f}")
print(f"  Current Market Price:    €{current_price:,.2f}")
print("  ─────────────────────────────────────")

if premium_discount > 0:
    print(f"  Market Premium:          {premium_discount:+.1%}")
    print("\n  → Stock appears OVERVALUED vs DCF estimate")
else:
    print(f"  Market Discount:         {premium_discount:+.1%}")
    print("\n  → Stock appears UNDERVALUED vs DCF estimate")

# Store final results
dcf_params["equity_value"] = equity_value
dcf_params["shares_outstanding"] = shares_outstanding
dcf_params["intrinsic_value_per_share"] = intrinsic_value_per_share
dcf_params["current_price"] = current_price
dcf_params["premium_discount"] = premium_discount
Out[158]:

======================================================================
EQUITY VALUE BRIDGE
======================================================================
  Enterprise Value (DCF):  €     107.5B
  Less: Net Debt:          €      50.0B
  ─────────────────────────────────────
  Equity Value:            €      57.5B

  Shares Outstanding:      556M
  ─────────────────────────────────────
  Intrinsic Value/Share:   €    103.36

======================================================================
VALUATION SUMMARY
======================================================================

  DCF Intrinsic Value:     €103.36
  Current Market Price:    €85.08
  ─────────────────────────────────────
  Market Discount:         -17.7%

  → Stock appears UNDERVALUED vs DCF estimate
In [159]:
# =============================================================================
# Figure 4: DCF Valuation Summary Visualization
# =============================================================================

fig = plots.dcf_valuation_summary(
    pv_forecast_fcfs,
    pv_terminal_value,
    enterprise_value_dcf,
    net_debt_valuation,
    equity_value,
    intrinsic_value_per_share,
    current_price,
    ticker
)
fig.show()
Out[159]:

6. Sensitivity Analysis

DCF valuations are highly sensitive to key assumptions. We analyze the impact of varying:

  1. WACC (7.5% – 10%): Reflects uncertainty in cost of capital estimation (base case: ~8.2%)
  2. Terminal Growth Rate (0.5% – 2.0%): Long-run growth assumptions (base case: 1.5%)

The sensitivity matrix shows intrinsic value per share across different scenarios:

6.1 WACC–Growth Sensitivity Matrix

In [160]:
# =============================================================================
# Sensitivity Analysis: WACC vs Terminal Growth Rate (Middle-of-Road Ranges)
# =============================================================================


# -----------------------------------------------------------------------------
# Define Sensitivity Ranges (middle-of-road: WACC 8.5-9%, growth 1.2-1.5%)
# -----------------------------------------------------------------------------
wacc_range = [0.075, 0.08, 0.085, 0.09, 0.095, 0.10]
growth_range = [0.005, 0.010, 0.0135, 0.015, 0.020]

# Get forecast FCFs and terminal FCF from previous calculations
forecast_fcfs = df_forecast["FCF"].values
terminal_fcf_base = df_forecast["FCF"].iloc[-1]
net_debt_val = dcf_params["net_debt"]
shares = info.get("sharesOutstanding", 600_000_000)


# -----------------------------------------------------------------------------
# Build Sensitivity Matrix
# -----------------------------------------------------------------------------
def calculate_dcf_value(wacc_val, growth_val, fcfs, terminal_fcf, net_debt, shares_out):
    """Calculate intrinsic value per share for given WACC and growth rate."""
    pv_fcfs = sum(fcf / (1 + wacc_val) ** (t + 1) for t, fcf in enumerate(fcfs))
    tv = terminal_fcf * (1 + growth_val) / (wacc_val - growth_val)
    pv_tv = tv / (1 + wacc_val) ** len(fcfs)
    ev = pv_fcfs + pv_tv
    equity_val = ev - net_debt
    return equity_val / shares_out


# Generate sensitivity matrix
sensitivity_matrix = pd.DataFrame(
    [
        [
            calculate_dcf_value(w, g, forecast_fcfs, terminal_fcf_base, net_debt_val, shares)
            for g in growth_range
        ]
        for w in wacc_range
    ],
    index=[f"{w:.1%}" for w in wacc_range],
    columns=[f"{g:.1%}" for g in growth_range],
)

# Store for later
dcf_params["sensitivity_matrix"] = sensitivity_matrix
dcf_params["calculate_dcf_value"] = calculate_dcf_value
In [161]:
# =============================================================================
# Figure 5: Sensitivity Analysis Visualization
# =============================================================================

# Calculate tornado data for key drivers
base_value = intrinsic_value_per_share

# WACC sensitivity (7.5% to 10% range)
wacc_low = calculate_dcf_value(
    0.075, terminal_growth_rate, forecast_fcfs, terminal_fcf_base, net_debt_val, shares
)
wacc_high = calculate_dcf_value(
    0.10, terminal_growth_rate, forecast_fcfs, terminal_fcf_base, net_debt_val, shares
)

# Growth sensitivity (0.5% to 2.0% range)
growth_low = calculate_dcf_value(wacc, 0.005, forecast_fcfs, terminal_fcf_base, net_debt_val, shares)
growth_high = calculate_dcf_value(
    wacc, 0.02, forecast_fcfs, terminal_fcf_base, net_debt_val, shares
)

# FCF sensitivity (±20%)
fcf_low = calculate_dcf_value(
    wacc, terminal_growth_rate, forecast_fcfs, terminal_fcf_base * 0.8, net_debt_val, shares
)
fcf_high = calculate_dcf_value(
    wacc, terminal_growth_rate, forecast_fcfs, terminal_fcf_base * 1.2, net_debt_val, shares
)

tornado_data = [
    {
        "var": "WACC<br>(7.5% → 10%)",
        "low": wacc_high,
        "high": wacc_low,
        "range": abs(wacc_low - wacc_high),
    },
    {
        "var": "Terminal Growth<br>(0.5% → 2%)",
        "low": growth_low,
        "high": growth_high,
        "range": abs(growth_high - growth_low),
    },
    {
        "var": "Terminal FCF<br>(±20%)",
        "low": fcf_low,
        "high": fcf_high,
        "range": abs(fcf_high - fcf_low),
    },
]
tornado_data.sort(key=lambda x: x["range"], reverse=True)

# Create visualization
fig = plots.sensitivity_analysis_charts(
    sensitivity_matrix,
    wacc,
    terminal_growth_rate,
    intrinsic_value_per_share,
    tornado_data,
    ticker
)
fig.show()
Out[161]:

6.2 Scenario Analysis

We define three scenarios reflecting different assumptions about BMW's EV transition:

Assumed Values by Scenario

Scenario WACC Terminal Growth Terminal Margin Net Debt Rationale
Bull 8.3% 1.45% 8.7% €48B Good EV execution; margin recovery; lower leverage
Base 8.5% 1.3% 8.2% €50B Moderate recovery; stable capital structure
Bear 8.7% 1.15% 7.8% €51B Slower growth; margin pressure; slightly higher debt

Note on DCF Sensitivity: We use narrow parameter spreads (WACC ±0.2%, growth ±0.15%) to produce mathematically plausible valuations. The Gordon Growth terminal value formula divides by (WACC − g), making DCF models extremely sensitive to these inputs. Wide parameter ranges (e.g., WACC 8.0%–9.5%, growth 0.8%–1.8%) produce implausible extremes (€6–€128).

In [162]:
# =============================================================================
# Scenario Analysis: Bull / Base / Bear Cases
# =============================================================================


def run_full_dcf_scenario(
    base_rev,
    rev_growth_rates,
    margin_trajectory,
    capex_pcts,
    da_pcts,
    tax_r,
    wacc_val,
    term_growth,
    net_debt_val,
    shares_out,
):
    """Run a complete DCF model with given assumptions."""
    fcfs = []
    prev_rev = base_rev
    for i in range(5):
        rev = prev_rev * (1 + rev_growth_rates[i])
        delta_rev = rev - prev_rev
        ebit = rev * margin_trajectory[i]
        nopat_val = ebit * (1 - tax_r)
        da_val = rev * da_pcts[i]
        capex_val = rev * capex_pcts[i]
        delta_nwc_val = delta_rev * 0.15  # 15% working capital drag
        fcf_val = nopat_val + da_val - capex_val - delta_nwc_val
        fcfs.append(fcf_val)
        prev_rev = rev
    term_fcf = fcfs[-1]
    return calculate_dcf_value(wacc_val, term_growth, fcfs, term_fcf, net_debt_val, shares_out)


# Use adjusted industrial net debt (not captive finance debt)
industrial_net_debt = dcf_params["net_debt"]  # €50B adjusted

# -----------------------------------------------------------------------------
# Scenario definitions with NARROW parameter spreads for realistic valuations
# DCF models are highly sensitive - wide spreads produce implausible extremes
# -----------------------------------------------------------------------------
scenarios = {
    "Bull": {
        "wacc": 0.083,  # 8.3% - slightly lower risk premium
        "growth": 0.0145,  # 1.45% terminal growth
        "rev_growth": [0.015, 0.018, 0.020, 0.020, 0.020],
        "margins": [0.078, 0.082, 0.085, 0.087, 0.087],  # 7.8% → 8.7%
        "capex": [0.082, 0.078, 0.075, 0.072, 0.070],
        "da": [0.062, 0.065, 0.068, 0.070, 0.070],
        "net_debt": 48_000_000_000,  # €48B - lower leverage in bull case
        "color": "rgb(44, 160, 101)",
    },
    "Base": {
        "wacc": 0.085,  # 8.5% WACC
        "growth": 0.013,  # 1.3% terminal growth
        "rev_growth": [0.010, 0.015, 0.015, 0.018, 0.018],
        "margins": [0.075, 0.078, 0.080, 0.082, 0.082],  # 7.5% → 8.2%
        "capex": [0.085, 0.082, 0.078, 0.075, 0.072],
        "da": [0.060, 0.065, 0.068, 0.070, 0.070],
        "net_debt": 50_000_000_000,  # €50B - stable
        "color": "rgb(55, 83, 109)",
    },
    "Bear": {
        "wacc": 0.087,  # 8.7% - slightly higher risk
        "growth": 0.0115,  # 1.15% terminal growth
        "rev_growth": [0.008, 0.010, 0.012, 0.015, 0.015],
        "margins": [0.072, 0.074, 0.076, 0.078, 0.078],  # 7.2% → 7.8%
        "capex": [0.086, 0.083, 0.080, 0.078, 0.076],
        "da": [0.062, 0.066, 0.070, 0.072, 0.072],
        "net_debt": 51_000_000_000,  # €51B - slightly higher leverage
        "color": "rgb(214, 39, 40)",
    },
}

# Calculate values using scenario-specific net debt
for _name, params in scenarios.items():
    scenario_net_debt = params.get("net_debt", industrial_net_debt)
    params["value"] = run_full_dcf_scenario(
        base_revenue,
        params["rev_growth"],
        params["margins"],
        params["capex"],
        params["da"],
        tax_rate,
        params["wacc"],
        params["growth"],
        scenario_net_debt,  # Use scenario-specific net debt
        shares,
    )

# Probability-weighted expected value
prob_weights = {"Bull": 0.25, "Base": 0.50, "Bear": 0.25}
expected_value = sum(scenarios[s]["value"] * prob_weights[s] for s in scenarios)

dcf_params["scenarios"] = scenarios
dcf_params["expected_value"] = expected_value

print(f"Scenario Values:")
print(f"  Bull Case:  €{scenarios['Bull']['value']:.2f}/share (+{(scenarios['Bull']['value']/current_price-1)*100:.0f}%)")
print(f"  Base Case:  €{scenarios['Base']['value']:.2f}/share (+{(scenarios['Base']['value']/current_price-1)*100:.0f}%)")
print(f"  Bear Case:  €{scenarios['Bear']['value']:.2f}/share ({(scenarios['Bear']['value']/current_price-1)*100:.0f}%)")
print(f"  Expected:   €{expected_value:.2f}/share (+{(expected_value/current_price-1)*100:.0f}%)")
Out[162]:
Scenario Values:
  Bull Case:  €125.27/share (+47%)
  Base Case:  €88.99/share (+5%)
  Bear Case:  €63.77/share (-25%)
  Expected:   €91.76/share (+8%)
In [163]:
# =============================================================================
# Figure 6: Scenario Analysis & Final Valuation Summary
# =============================================================================

premium_discount = (expected_value / current_price) - 1

fig = plots.scenario_analysis_summary(
    scenarios,
    prob_weights,
    expected_value,
    current_price,
    premium_discount,
    ticker
)
fig.show()
Out[163]:

7. Conclusion

7.1 Investment Verdict

Based on our DCF analysis with balanced assumptions, we summarize the key findings:

Criterion Assessment
Methodology Two-stage DCF with 5-year explicit forecast + terminal value
Base Case Value ~€89/share
Scenario Range €64 (Bear) – €125 (Bull)
Current Price ~€85/share
Upside Potential ~5% (Base Case)
Key Drivers WACC (8.3–8.7%), terminal growth (1.15–1.45%), EBIT margin trajectory
Sensitivity High — DCF models are inherently sensitive to terminal value assumptions

7.2 Risk Factors

Risk Impact Mitigation
EV transition costs FCF pressure Monitor CapEx trends
Margin compression Lower terminal value Track quarterly margins
Macro slowdown Higher WACC, lower growth Diversified geographic exposure
Competition (Tesla, Chinese OEMs) Market share loss Brand strength, Neue Klasse platform

7.3 Monitoring Metrics

  1. EBIT Margin — Track recovery/stabilization in 8–9% range
  2. FCF — Monitor trajectory vs. forecast assumptions
  3. ROIC vs WACC — Spread must remain positive for value creation
  4. EV Unit Sales — Neue Klasse adoption trajectory

7.4 Final Assessment

BMW appears fairly valued at current market levels. The Base Case suggests ~5% upside, while the scenario range of €64–€125 reflects realistic uncertainty bounds. Key considerations:

  • Terminal value concentration (~75% of EV) makes the model highly sensitive to WACC and growth assumptions
  • Narrow parameter spreads (WACC 8.3–8.7%, growth 1.15–1.45%) are required to produce mathematically plausible valuations
  • EBIT margin trajectory (7.2–8.7%) is the primary driver of FCF differences across scenarios

Note on DCF Sensitivity: Wide parameter ranges produce extreme valuations that are mathematically unstable due to the Gordon Growth formula's division by (WACC − g). A spread of only 7% (e.g., WACC 8.5% − g 1.5%) implies a 14× terminal FCF multiple.

In [164]:
# =============================================================================
# Figure 7: Executive Dashboard — Full DCF Model Summary
# =============================================================================

fig = plots.executive_dashboard_gauges(
    rf,
    cost_of_equity,
    wacc,
    intrinsic_value_per_share,
    current_price,
    sensitivity_matrix,
    ticker
)
fig.show()
Out[164]: