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:
- Data Collection: Extraction of financial statements and market data via standardized API interfaces
- Historical Analysis: Examination of profitability trends, capital allocation, and cash flow generation
- Peer Benchmarking: Comparative assessment against industry participants to contextualize findings
All computational procedures are implemented using documented, version-controlled code to ensure reproducibility.
# =============================================================================
# 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()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.
# =============================================================================
# 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"))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...
| 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:
- Market Data: Daily Open-High-Low-Close-Volume (OHLCV) prices
- Income Statement: Revenue, EBIT, Tax Provision, Net Income
- Balance Sheet: Total Equity, Total Debt, Cash and Equivalents
- Cash Flow Statement: Operating Cash Flow, Capital Expenditures
- 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.
# =============================================================================
# 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")
)| 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% |
# =============================================================================
# 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).year4. 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 |
# =============================================================================
# Figure 1: Financial Performance Dashboard
# =============================================================================
fig = plots.financial_performance_dashboard(
df_annual,
df_quarterly,
df_prices,
peer_metrics,
ticker
)
fig.show()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 ( |
| 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.
# =============================================================================
# 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"))| 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 |
# =============================================================================
# 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"))
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:
# =============================================================================
# 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]# =============================================================================
# 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"))
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
# =============================================================================
# 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============================================================
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:
# =============================================================================
# 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======================================================================
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%
# =============================================================================
# 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
======================================================================
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
# =============================================================================
# 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()6. Sensitivity Analysis
DCF valuations are highly sensitive to key assumptions. We analyze the impact of varying:
- WACC (7.5% – 10%): Reflects uncertainty in cost of capital estimation (base case: ~8.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
# =============================================================================
# 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# =============================================================================
# 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()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).
# =============================================================================
# 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}%)")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%)
# =============================================================================
# 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()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
- EBIT Margin — Track recovery/stabilization in 8–9% range
- FCF — Monitor trajectory vs. forecast assumptions
- ROIC vs WACC — Spread must remain positive for value creation
- 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.
# =============================================================================
# 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()