Module 8: Financial Modeling Fundamentals
Learning Objectives
By the end of this module, you will:
- Understand time value of money concepts and calculations
- Build discounted cash flow (DCF) models
- Perform bond pricing and yield calculations
- Value stocks using dividend discount models
- Calculate option prices with Black-Scholes
- Run Monte Carlo simulations for risk assessment
- Create sensitivity and scenario analysis
- Build complete financial forecasting models
- Automate valuation with Python
8.1 Time Value of Money
The Foundation of Finance
Money today is worth more than money tomorrow. This fundamental principle underlies all financial modeling. Why?
- Opportunity Cost: Money today can be invested to earn returns
- Inflation: Money loses purchasing power over time
- Risk: Future payments are uncertain
Present Value (PV)
The current value of a future cash flow.
Formula: PV = FV / (1 + r)^n
Where:
- FV = Future Value
- r = Discount rate (required return)
- n = Number of periods
import numpy as np
import pandas as pd
def present_value(future_value, rate, periods):
"""
Calculate present value of a future cash flow
"""
pv = future_value / (1 + rate) ** periods
return pv
# Example: What is $10,000 in 5 years worth today at 8% discount rate?
fv = 10000
rate = 0.08
periods = 5
pv = present_value(fv, rate, periods)
print(f"Future Value: ${fv:,.2f}")
print(f"Discount Rate: {rate*100:.1f}%")
print(f"Time Period: {periods} years")
print(f"Present Value: ${pv:,.2f}")
# Verify by calculating future value
fv_check = pv * (1 + rate) ** periods
print(f"\nVerification: ${pv:,.2f} invested at {rate*100:.1f}% for {periods} years = ${fv_check:,.2f}")
Future Value (FV)
The value of money today at a point in the future.
Formula: FV = PV × (1 + r)^n
def future_value(present_value, rate, periods):
"""
Calculate future value of a present cash flow
"""
fv = present_value * (1 + rate) ** periods
return fv
# Example: If you invest $5,000 today at 7% for 10 years
pv = 5000
rate = 0.07
periods = 10
fv = future_value(pv, rate, periods)
print(f"Initial Investment: ${pv:,.2f}")
print(f"Annual Return: {rate*100:.1f}%")
print(f"Time Period: {periods} years")
print(f"Future Value: ${fv:,.2f}")
print(f"Total Gain: ${fv - pv:,.2f}")
print(f"Percentage Gain: {((fv/pv - 1)*100):.2f}%")
Net Present Value (NPV)
NPV is the sum of all present values of cash flows (including initial investment).
def npv(rate, cash_flows):
"""
Calculate Net Present Value
cash_flows: list where first element is initial investment (negative)
"""
npv_value = 0
for t, cf in enumerate(cash_flows):
npv_value += cf / (1 + rate) ** t
return npv_value
# Example: Investment project
# Year 0: -$100,000 (initial investment)
# Years 1-5: $30,000 per year (returns)
cash_flows = [-100000, 30000, 30000, 30000, 30000, 30000]
discount_rate = 0.10
project_npv = npv(discount_rate, cash_flows)
print("Investment Project Analysis")
print("="*50)
print(f"Initial Investment: ${abs(cash_flows[0]):,.2f}")
print(f"Annual Returns: ${cash_flows[1]:,.2f} for {len(cash_flows)-1} years")
print(f"Discount Rate: {discount_rate*100:.1f}%")
print(f"\nNPV: ${project_npv:,.2f}")
if project_npv > 0:
print("Decision: ACCEPT (NPV is positive)")
else:
print("Decision: REJECT (NPV is negative)")
# Using NumPy's built-in NPV function
import numpy_financial as npf
npv_numpy = npf.npv(discount_rate, cash_flows)
print(f"\nNPV (NumPy verification): ${npv_numpy:,.2f}")
Internal Rate of Return (IRR)
IRR is the discount rate that makes NPV equal to zero. It represents the project's actual return.
def irr(cash_flows, guess=0.1):
"""
Calculate Internal Rate of Return using Newton's method
"""
return npf.irr(cash_flows)
# Calculate IRR for the investment project
project_irr = irr(cash_flows)
print(f"\nInternal Rate of Return: {project_irr*100:.2f}%")
print(f"Required Return: {discount_rate*100:.1f}%")
if project_irr > discount_rate:
print("Decision: ACCEPT (IRR exceeds required return)")
else:
print("Decision: REJECT (IRR below required return)")
Annuities
A series of equal payments at regular intervals.
def pv_annuity(payment, rate, periods):
"""
Present value of an annuity
PV = PMT × [(1 - (1 + r)^-n) / r]
"""
pv = payment * ((1 - (1 + rate) ** -periods) / rate)
return pv
def fv_annuity(payment, rate, periods):
"""
Future value of an annuity
FV = PMT × [((1 + r)^n - 1) / r]
"""
fv = payment * (((1 + rate) ** periods - 1) / rate)
return fv
# Example: Retirement savings
monthly_contribution = 500
annual_rate = 0.08
monthly_rate = annual_rate / 12
years = 30
months = years * 12
retirement_fv = fv_annuity(monthly_contribution, monthly_rate, months)
print("Retirement Savings Calculation")
print("="*50)
print(f"Monthly Contribution: ${monthly_contribution:,.2f}")
print(f"Annual Return: {annual_rate*100:.1f}%")
print(f"Time Period: {years} years")
print(f"\nFuture Value: ${retirement_fv:,.2f}")
print(f"Total Contributions: ${monthly_contribution * months:,.2f}")
print(f"Investment Gains: ${retirement_fv - (monthly_contribution * months):,.2f}")
8.2 Bond Pricing and Yield
Understanding Bonds
Bonds are fixed-income securities that pay regular coupon payments and return principal at maturity.
Bond Price Calculation
def bond_price(face_value, coupon_rate, market_rate, years_to_maturity, frequency=2):
"""
Calculate bond price
Parameters:
- face_value: Par value of bond (typically $1,000)
- coupon_rate: Annual coupon rate (as decimal)
- market_rate: Required yield/market rate (as decimal)
- years_to_maturity: Years until maturity
- frequency: Payments per year (2 for semi-annual)
"""
periods = int(years_to_maturity * frequency)
coupon_payment = (face_value * coupon_rate) / frequency
period_rate = market_rate / frequency
# Present value of coupon payments (annuity)
pv_coupons = coupon_payment * ((1 - (1 + period_rate) ** -periods) / period_rate)
# Present value of face value
pv_face = face_value / (1 + period_rate) ** periods
# Bond price
price = pv_coupons + pv_face
return price
# Example: 10-year bond
face_value = 1000
coupon_rate = 0.05 # 5% annual coupon
market_rate = 0.06 # 6% market yield
years = 10
price = bond_price(face_value, coupon_rate, market_rate, years)
print("Bond Valuation")
print("="*50)
print(f"Face Value: ${face_value:,.2f}")
print(f"Coupon Rate: {coupon_rate*100:.1f}%")
print(f"Market Yield: {market_rate*100:.1f}%")
print(f"Years to Maturity: {years}")
print(f"\nBond Price: ${price:.2f}")
if price < face_value:
print("Bond is trading at a DISCOUNT")
print(f"Discount: ${face_value - price:.2f}")
elif price > face_value:
print("Bond is trading at a PREMIUM")
print(f"Premium: ${price - face_value:.2f}")
else:
print("Bond is trading at PAR")
Current Yield and Yield to Maturity
def current_yield(face_value, coupon_rate, price):
"""
Current Yield = Annual Coupon Payment / Current Price
"""
annual_coupon = face_value * coupon_rate
return annual_coupon / price
def yield_to_maturity(face_value, coupon_rate, price, years_to_maturity, frequency=2):
"""
Calculate YTM (approximate using Newton's method)
"""
# We'll use a numerical solver
from scipy.optimize import fsolve
periods = int(years_to_maturity * frequency)
coupon_payment = (face_value * coupon_rate) / frequency
def bond_price_error(ytm):
period_rate = ytm / frequency
pv_coupons = coupon_payment * ((1 - (1 + period_rate) ** -periods) / period_rate)
pv_face = face_value / (1 + period_rate) ** periods
return pv_coupons + pv_face - price
ytm = fsolve(bond_price_error, 0.05)[0]
return ytm
# Calculate yields
current_yld = current_yield(face_value, coupon_rate, price)
ytm = yield_to_maturity(face_value, coupon_rate, price, years)
print(f"\nCurrent Yield: {current_yld*100:.2f}%")
print(f"Yield to Maturity: {ytm*100:.2f}%")
print(f"Market Rate (for reference): {market_rate*100:.2f}%")
Duration and Convexity
Duration measures bond price sensitivity to interest rate changes.
def macaulay_duration(face_value, coupon_rate, market_rate, years_to_maturity, frequency=2):
"""
Calculate Macaulay Duration
"""
periods = int(years_to_maturity * frequency)
coupon_payment = (face_value * coupon_rate) / frequency
period_rate = market_rate / frequency
# Calculate weighted present value of cash flows
weighted_pv = 0
for t in range(1, periods + 1):
pv = coupon_payment / (1 + period_rate) ** t
weighted_pv += t * pv
# Add principal payment
pv_principal = face_value / (1 + period_rate) ** periods
weighted_pv += periods * pv_principal
# Bond price
bond_prc = bond_price(face_value, coupon_rate, market_rate, years_to_maturity, frequency)
# Macaulay duration (in periods)
duration_periods = weighted_pv / bond_prc
# Convert to years
duration_years = duration_periods / frequency
return duration_years
def modified_duration(macaulay_dur, market_rate, frequency=2):
"""
Modified Duration = Macaulay Duration / (1 + YTM/frequency)
"""
return macaulay_dur / (1 + market_rate / frequency)
# Calculate duration
mac_duration = macaulay_duration(face_value, coupon_rate, market_rate, years)
mod_duration = modified_duration(mac_duration, market_rate)
print("\nDuration Analysis")
print("="*50)
print(f"Macaulay Duration: {mac_duration:.2f} years")
print(f"Modified Duration: {mod_duration:.2f}")
# Estimate price change for 1% rate increase
rate_change = 0.01 # 1% increase
estimated_price_change = -mod_duration * rate_change * price
print(f"\nIf rates increase by {rate_change*100:.0f}%:")
print(f"Estimated price change: ${estimated_price_change:.2f}")
print(f"Estimated new price: ${price + estimated_price_change:.2f}")
# Actual new price
new_price = bond_price(face_value, coupon_rate, market_rate + rate_change, years)
print(f"Actual new price: ${new_price:.2f}")
print(f"Estimation error: ${abs((price + estimated_price_change) - new_price):.2f}")
8.3 Stock Valuation Models
Dividend Discount Model (DDM)
Values a stock based on the present value of future dividends.
def gordon_growth_model(dividend, growth_rate, required_return):
"""
Gordon Growth Model (Constant Growth DDM)
P0 = D1 / (r - g)
where:
- D1 = Expected dividend next year
- r = Required return
- g = Constant growth rate
"""
if required_return <= growth_rate:
raise ValueError("Required return must be greater than growth rate")
price = dividend / (required_return - growth_rate)
return price
# Example: Valuing a dividend-paying stock
current_dividend = 2.50 # Last dividend paid
growth_rate = 0.05 # 5% annual growth
required_return = 0.10 # 10% required return
# Next year's dividend
next_dividend = current_dividend * (1 + growth_rate)
intrinsic_value = gordon_growth_model(next_dividend, growth_rate, required_return)
print("Dividend Discount Model Valuation")
print("="*50)
print(f"Current Dividend: ${current_dividend:.2f}")
print(f"Growth Rate: {growth_rate*100:.1f}%")
print(f"Required Return: {required_return*100:.1f}%")
print(f"Next Year's Dividend: ${next_dividend:.2f}")
print(f"\nIntrinsic Value: ${intrinsic_value:.2f}")
# Sensitivity analysis
print("\nSensitivity to Growth Rate:")
for g in [0.03, 0.04, 0.05, 0.06, 0.07]:
d1 = current_dividend * (1 + g)
value = gordon_growth_model(d1, g, required_return)
print(f" {g*100:.0f}% growth: ${value:.2f}")
Multi-Stage DDM
For companies with changing growth rates.
def multi_stage_ddm(current_dividend, high_growth_rate, high_growth_years,
stable_growth_rate, required_return):
"""
Two-stage dividend discount model
Stage 1: High growth period
Stage 2: Stable growth period (perpetuity)
"""
# Stage 1: High growth dividends
pv_high_growth = 0
dividend = current_dividend
for year in range(1, high_growth_years + 1):
dividend = dividend * (1 + high_growth_rate)
pv = dividend / (1 + required_return) ** year
pv_high_growth += pv
# Stage 2: Stable growth (terminal value)
terminal_dividend = dividend * (1 + stable_growth_rate)
terminal_value = terminal_dividend / (required_return - stable_growth_rate)
pv_terminal = terminal_value / (1 + required_return) ** high_growth_years
# Total intrinsic value
intrinsic_value = pv_high_growth + pv_terminal
return intrinsic_value, pv_high_growth, pv_terminal
# Example: Growth company
current_div = 1.00
high_growth = 0.15 # 15% for 5 years
high_years = 5
stable_growth = 0.04 # 4% thereafter
req_return = 0.11
value, pv_stage1, pv_stage2 = multi_stage_ddm(
current_div, high_growth, high_years, stable_growth, req_return
)
print("\nTwo-Stage DDM Valuation")
print("="*50)
print(f"Current Dividend: ${current_div:.2f}")
print(f"High Growth Rate: {high_growth*100:.0f}% for {high_years} years")
print(f"Stable Growth Rate: {stable_growth*100:.0f}%")
print(f"Required Return: {req_return*100:.0f}%")
print(f"\nPV of High Growth Stage: ${pv_stage1:.2f}")
print(f"PV of Terminal Value: ${pv_stage2:.2f}")
print(f"Total Intrinsic Value: ${value:.2f}")
print(f"\nTerminal value is {(pv_stage2/value)*100:.1f}% of total value")
8.4 Discounted Cash Flow (DCF) Model
Free Cash Flow Valuation
The most widely used valuation method in professional finance.
def dcf_valuation(free_cash_flows, wacc, terminal_growth_rate):
"""
DCF Valuation Model
Parameters:
- free_cash_flows: List of projected FCF for explicit forecast period
- wacc: Weighted Average Cost of Capital (discount rate)
- terminal_growth_rate: Perpetual growth rate after forecast period
"""
# Present value of forecast period cash flows
pv_forecast = 0
for year, fcf in enumerate(free_cash_flows, start=1):
pv = fcf / (1 + wacc) ** year
pv_forecast += pv
# Terminal value (Gordon Growth)
final_fcf = free_cash_flows[-1]
terminal_fcf = final_fcf * (1 + terminal_growth_rate)
terminal_value = terminal_fcf / (wacc - terminal_growth_rate)
# Present value of terminal value
forecast_years = len(free_cash_flows)
pv_terminal = terminal_value / (1 + wacc) ** forecast_years
# Enterprise value
enterprise_value = pv_forecast + pv_terminal
return {
'enterprise_value': enterprise_value,
'pv_forecast': pv_forecast,
'pv_terminal': pv_terminal,
'terminal_value': terminal_value
}
# Example: Company valuation
# Project 5 years of free cash flows
fcf_projections = [100, 115, 132, 150, 170] # in millions
wacc = 0.09 # 9%
terminal_growth = 0.03 # 3%
valuation = dcf_valuation(fcf_projections, wacc, terminal_growth)
print("DCF Valuation")
print("="*60)
print("\nProjected Free Cash Flows (millions):")
for year, fcf in enumerate(fcf_projections, start=1):
print(f" Year {year}: ${fcf:.0f}")
print(f"\nWACC: {wacc*100:.1f}%")
print(f"Terminal Growth Rate: {terminal_growth*100:.1f}%")
print(f"\nEnterprise Value: ${valuation['enterprise_value']:.2f}M")
print(f" PV of Forecast Period: ${valuation['pv_forecast']:.2f}M")
print(f" PV of Terminal Value: ${valuation['pv_terminal']:.2f}M")
print(f"\nTerminal value represents {(valuation['pv_terminal']/valuation['enterprise_value'])*100:.1f}% of total value")
# Calculate equity value
debt = 200 # millions
cash = 50 # millions
shares_outstanding = 100 # millions
equity_value = valuation['enterprise_value'] - debt + cash
price_per_share = equity_value / shares_outstanding
print(f"\nEquity Value Calculation:")
print(f" Enterprise Value: ${valuation['enterprise_value']:.2f}M")
print(f" Less: Debt: ${debt:.0f}M")
print(f" Plus: Cash: ${cash:.0f}M")
print(f" Equity Value: ${equity_value:.2f}M")
print(f"\nShares Outstanding: {shares_outstanding:.0f}M")
print(f"Intrinsic Value per Share: ${price_per_share:.2f}")
Building a Complete DCF Model
import pandas as pd
import numpy as np
class DCFModel:
"""
Complete DCF valuation model with financial projections
"""
def __init__(self, company_name, shares_outstanding, debt, cash):
self.company = company_name
self.shares = shares_outstanding
self.debt = debt
self.cash = cash
self.projections = None
def build_projections(self, base_revenue, revenue_growth_rates,
ebitda_margin, tax_rate, capex_percent_revenue,
nwc_percent_revenue, depreciation_percent_revenue):
"""
Build 5-year financial projections
"""
years = len(revenue_growth_rates)
projections = pd.DataFrame(index=range(1, years + 1))
# Revenue
projections['Revenue'] = [base_revenue]
for i in range(1, years):
projections.loc[i+1, 'Revenue'] = (
projections.loc[i, 'Revenue'] * (1 + revenue_growth_rates[i])
)
# EBITDA
projections['EBITDA'] = projections['Revenue'] * ebitda_margin
# Depreciation
projections['Depreciation'] = projections['Revenue'] * depreciation_percent_revenue
# EBIT
projections['EBIT'] = projections['EBITDA'] - projections['Depreciation']
# Taxes
projections['Taxes'] = projections['EBIT'] * tax_rate
# NOPAT (Net Operating Profit After Tax)
projections['NOPAT'] = projections['EBIT'] - projections['Taxes']
# Add back depreciation
projections['Plus_Depreciation'] = projections['Depreciation']
# CapEx
projections['CapEx'] = projections['Revenue'] * capex_percent_revenue
# Change in NWC
projections['NWC'] = projections['Revenue'] * nwc_percent_revenue
projections['Change_NWC'] = projections['NWC'].diff().fillna(0)
# Free Cash Flow
projections['FCF'] = (projections['NOPAT'] +
projections['Plus_Depreciation'] -
projections['CapEx'] -
projections['Change_NWC'])
self.projections = projections
return projections
def calculate_valuation(self, wacc, terminal_growth_rate):
"""
Calculate DCF valuation
"""
if self.projections is None:
raise ValueError("Build projections first")
fcf_list = self.projections['FCF'].tolist()
valuation = dcf_valuation(fcf_list, wacc, terminal_growth_rate)
# Calculate equity value and price per share
equity_value = valuation['enterprise_value'] - self.debt + self.cash
price_per_share = equity_value / self.shares
results = {
**valuation,
'equity_value': equity_value,
'price_per_share': price_per_share
}
return results
def sensitivity_analysis(self, wacc_range, growth_range):
"""
Create sensitivity table for different WACC and growth assumptions
"""
sensitivity = pd.DataFrame(
index=[f"{g*100:.1f}%" for g in growth_range],
columns=[f"{w*100:.1f}%" for w in wacc_range]
)
for i, growth in enumerate(growth_range):
for j, wacc in enumerate(wacc_range):
results = self.calculate_valuation(wacc, growth)
sensitivity.iloc[i, j] = results['price_per_share']
return sensitivity.astype(float)
def print_summary(self, wacc, terminal_growth):
"""
Print valuation summary
"""
results = self.calculate_valuation(wacc, terminal_growth)
print(f"\n{'='*70}")
print(f"DCF VALUATION: {self.company}")
print(f"{'='*70}")
print("\nProjected Financials (millions):")
print(self.projections[['Revenue', 'EBITDA', 'EBIT', 'FCF']].to_string())
print(f"\n{'='*70}")
print("Valuation Results:")
print(f"{'='*70}")
print(f"Enterprise Value: ${results['enterprise_value']:.2f}M")
print(f" PV of Forecast FCF: ${results['pv_forecast']:.2f}M")
print(f" PV of Terminal Value: ${results['pv_terminal']:.2f}M")
print(f"\nEquity Value: ${results['equity_value']:.2f}M")
print(f" Debt: ${self.debt:.0f}M")
print(f" Cash: ${self.cash:.0f}M")
print(f"\nShares Outstanding: {self.shares:.0f}M")
print(f"Intrinsic Value per Share: ${results['price_per_share']:.2f}")
print(f"{'='*70}")
# Example: Build complete DCF model
model = DCFModel(
company_name="TechCorp",
shares_outstanding=100, # millions
debt=200, # millions
cash=50 # millions
)
# Build 5-year projections
projections = model.build_projections(
base_revenue=1000, # Starting revenue in millions
revenue_growth_rates=[0.15, 0.12, 0.10, 0.08, 0.07],
ebitda_margin=0.30,
tax_rate=0.25,
capex_percent_revenue=0.05,
nwc_percent_revenue=0.10,
depreciation_percent_revenue=0.03
)
# Calculate valuation
model.print_summary(wacc=0.09, terminal_growth=0.03)
# Sensitivity analysis
print("\nSensitivity Analysis - Price per Share:")
print("="*70)
wacc_range = [0.07, 0.08, 0.09, 0.10, 0.11]
growth_range = [0.02, 0.025, 0.03, 0.035, 0.04]
sensitivity = model.sensitivity_analysis(wacc_range, growth_range)
print(sensitivity.to_string())
8.5 Option Pricing: Black-Scholes Model
Understanding Options
Options give the right (but not obligation) to buy (call) or sell (put) an asset at a specified price.
import numpy as np
from scipy.stats import norm
def black_scholes(S, K, T, r, sigma, option_type='call'):
"""
Black-Scholes Option Pricing Model
Parameters:
- S: Current stock price
- K: Strike price
- T: Time to maturity (in years)
- r: Risk-free rate
- sigma: Volatility (annualized)
- option_type: 'call' or 'put'
"""
# Calculate d1 and d2
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
if option_type == 'call':
# Call option price
price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
elif option_type == 'put':
# Put option price
price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
else:
raise ValueError("option_type must be 'call' or 'put'")
return price
# Example: Price a call option
stock_price = 100
strike_price = 105
time_to_maturity = 0.25 # 3 months
risk_free_rate = 0.05 # 5%
volatility = 0.25 # 25%
call_price = black_scholes(stock_price, strike_price, time_to_maturity,
risk_free_rate, volatility, 'call')
put_price = black_scholes(stock_price, strike_price, time_to_maturity,
risk_free_rate, volatility, 'put')
print("Black-Scholes Option Pricing")
print("="*50)
print(f"Stock Price: ${stock_price:.2f}")
print(f"Strike Price: ${strike_price:.2f}")
print(f"Time to Maturity: {time_to_maturity*12:.0f} months")
print(f"Risk-Free Rate: {risk_free_rate*100:.1f}%")
print(f"Volatility: {volatility*100:.0f}%")
print(f"\nCall Option Price: ${call_price:.2f}")
print(f"Put Option Price: ${put_price:.2f}")
# Verify put-call parity: C - P = S - K*e^(-rT)
parity_check = call_price - put_price
theoretical_diff = stock_price - strike_price * np.exp(-risk_free_rate * time_to_maturity)
print(f"\nPut-Call Parity Check:")
print(f"C - P = ${parity_check:.2f}")
print(f"S - K*e^(-rT) = ${theoretical_diff:.2f}")
print(f"Difference: ${abs(parity_check - theoretical_diff):.6f}")
The Greeks
Measure option price sensitivity to various factors.
def calculate_greeks(S, K, T, r, sigma, option_type='call'):
"""
Calculate option Greeks
"""
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
# Delta: Price sensitivity to underlying
if option_type == 'call':
delta = norm.cdf(d1)
else:
delta = norm.cdf(d1) - 1
# Gamma: Rate of change of delta
gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
# Theta: Time decay
if option_type == 'call':
theta = (-(S * norm.pdf(d1) * sigma) / (2 * np.sqrt(T)) -
r * K * np.exp(-r * T) * norm.cdf(d2)) / 365
else:
theta = (-(S * norm.pdf(d1) * sigma) / (2 * np.sqrt(T)) +
r * K * np.exp(-r * T) * norm.cdf(-d2)) / 365
# Vega: Volatility sensitivity
vega = S * norm.pdf(d1) * np.sqrt(T) / 100
# Rho: Interest rate sensitivity
if option_type == 'call':
rho = K * T * np.exp(-r * T) * norm.cdf(d2) / 100
else:
rho = -K * T * np.exp(-r * T) * norm.cdf(-d2) / 100
return {
'delta': delta,
'gamma': gamma,
'theta': theta,
'vega': vega,
'rho': rho
}
# Calculate Greeks
greeks = calculate_greeks(stock_price, strike_price, time_to_maturity,
risk_free_rate, volatility, 'call')
print("\nOption Greeks (Call):")
print("="*50)
print(f"Delta: {greeks['delta']:.4f}")
print(" (Option price changes by ${:.2f} for $1 change in stock)".format(
greeks['delta']))
print(f"\nGamma: {greeks['gamma']:.4f}")
print(" (Delta changes by {:.4f} for $1 change in stock)".format(
greeks['gamma']))
print(f"\nTheta: ${greeks['theta']:.4f}")
print(" (Option loses ${:.2f} in value per day)".format(abs(greeks['theta'])))
print(f"\nVega: {greeks['vega']:.4f}")
print(" (Option price changes by ${:.2f} for 1% change in volatility)".format(
greeks['vega']))
print(f"\nRho: {greeks['rho']:.4f}")
print(" (Option price changes by ${:.2f} for 1% change in rates)".format(
greeks['rho']))
8.6 Monte Carlo Simulation
Simulating Stock Prices
Monte Carlo simulation generates thousands of possible future scenarios.
import numpy as np
import matplotlib.pyplot as plt
def monte_carlo_stock_price(S0, mu, sigma, T, num_simulations, num_steps):
"""
Monte Carlo simulation for stock price paths
Parameters:
- S0: Initial stock price
- mu: Expected annual return
- sigma: Annual volatility
- T: Time horizon (years)
- num_simulations: Number of simulations to run
- num_steps: Number of time steps
"""
dt = T / num_steps
prices = np.zeros((num_steps + 1, num_simulations))
prices[0] = S0
for t in range(1, num_steps + 1):
# Generate random returns
random_returns = np.random.normal(
(mu - 0.5 * sigma**2) * dt,
sigma * np.sqrt(dt),
num_simulations
)
prices[t] = prices[t-1] * np.exp(random_returns)
return prices
# Run simulation
np.random.seed(42)
initial_price = 100
expected_return = 0.10 # 10%
volatility = 0.25 # 25%
time_horizon = 1 # 1 year
num_sims = 1000
num_steps = 252 # Daily steps
simulated_prices = monte_carlo_stock_price(
initial_price, expected_return, volatility,
time_horizon, num_sims, num_steps
)
# Plot simulations
plt.figure(figsize=(14, 7))
# Plot all paths (with transparency)
time_points = np.linspace(0, time_horizon, num_steps + 1)
for i in range(min(100, num_sims)): # Plot first 100 paths
plt.plot(time_points, simulated_prices[:, i], alpha=0.1, color='blue')
# Plot mean path
mean_path = simulated_prices.mean(axis=1)
plt.plot(time_points, mean_path, color='red', linewidth=2, label='Mean Path')
# Plot confidence intervals
percentile_5 = np.percentile(simulated_prices, 5, axis=1)
percentile_95 = np.percentile(simulated_prices, 95, axis=1)
plt.plot(time_points, percentile_5, color='green', linewidth=2,
linestyle='--', label='5th Percentile')
plt.plot(time_points, percentile_95, color='green', linewidth=2,
linestyle='--', label='95th Percentile')
plt.fill_between(time_points, percentile_5, percentile_95, alpha=0.2, color='green')
plt.xlabel('Time (years)', fontsize=12)
plt.ylabel('Stock Price ($)', fontsize=12)
plt.title(f'Monte Carlo Simulation: {num_sims} Price Paths',
fontsize=16, fontweight='bold', pad=20)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Analyze final distribution
final_prices = simulated_prices[-1, :]
print("Monte Carlo Simulation Results")
print("="*50)
print(f"Initial Price: ${initial_price:.2f}")
print(f"Expected Return: {expected_return*100:.0f}%")
print(f"Volatility: {volatility*100:.0f}%")
print(f"Time Horizon: {time_horizon} year")
print(f"Number of Simulations: {num_sims:,}")
print(f"\nFinal Price Distribution:")
print(f" Mean: ${final_prices.mean():.2f}")
print(f" Median: ${np.median(final_prices):.2f}")
print(f" Std Dev: ${final_prices.std():.2f}")
print(f" 5th Percentile: ${np.percentile(final_prices, 5):.2f}")
print(f" 95th Percentile: ${np.percentile(final_prices, 95):.2f}")
# Probability of profit
prob_profit = (final_prices > initial_price).sum() / num_sims
print(f"\nProbability of Profit: {prob_profit*100:.1f}%")
# Expected gain/loss
expected_value = final_prices.mean()
expected_gain = expected_value - initial_price
print(f"Expected Final Value: ${expected_value:.2f}")
print(f"Expected Gain: ${expected_gain:.2f} ({(expected_gain/initial_price)*100:.2f}%)")
Value at Risk (VaR) Using Monte Carlo
# Calculate VaR from simulation
returns = (final_prices / initial_price) - 1
# VaR at different confidence levels
var_95 = np.percentile(returns, 5)
var_99 = np.percentile(returns, 1)
# CVaR (Expected Shortfall)
cvar_95 = returns[returns <= var_95].mean()
cvar_99 = returns[returns <= var_99].mean()
print("\nValue at Risk Analysis:")
print("="*50)
print(f"95% VaR: {var_95*100:.2f}%")
print(f" Maximum loss (95% confidence): ${abs(var_95 * initial_price):.2f}")
print(f"\n99% VaR: {var_99*100:.2f}%")
print(f" Maximum loss (99% confidence): ${abs(var_99 * initial_price):.2f}")
print(f"\n95% CVaR: {cvar_95*100:.2f}%")
print(f" Expected loss when VaR is exceeded: ${abs(cvar_95 * initial_price):.2f}")
print(f"\n99% CVaR: {cvar_99*100:.2f}%")
print(f" Expected loss when VaR is exceeded: ${abs(cvar_99 * initial_price):.2f}")
8.7 Scenario and Sensitivity Analysis
Scenario Analysis
Evaluate outcomes under different scenarios.
def scenario_analysis(base_case, scenarios):
"""
Analyze different scenarios
scenarios: dict of scenario_name: {variable: value}
"""
results = {}
for scenario_name, variables in scenarios.items():
# Create scenario case from base case
case = base_case.copy()
case.update(variables)
# Calculate outcome (example: NPV)
cash_flows = case['cash_flows']
discount_rate = case['discount_rate']
npv_value = npv(discount_rate, cash_flows)
results[scenario_name] = {
'npv': npv_value,
'variables': case
}
return results
# Define base case
base_case = {
'cash_flows': [-1000, 300, 350, 400, 450, 500],
'discount_rate': 0.10
}
# Define scenarios
scenarios = {
'Base Case': {},
'Best Case': {
'cash_flows': [-1000, 350, 400, 450, 500, 550],
'discount_rate': 0.08
},
'Worst Case': {
'cash_flows': [-1000, 250, 280, 320, 360, 400],
'discount_rate': 0.12
},
'Low Growth': {
'cash_flows': [-1000, 280, 300, 320, 340, 360],
'discount_rate': 0.10
}
}
# Run scenario analysis
results = scenario_analysis(base_case, scenarios)
print("Scenario Analysis")
print("="*70)
for scenario_name, result in results.items():
npv_value = result['npv']
print(f"\n{scenario_name}:")
print(f" NPV: ${npv_value:,.2f}")
if npv_value > 0:
print(f" Decision: ACCEPT")
else:
print(f" Decision: REJECT")
Sensitivity Analysis
See how changes in variables affect outcomes.
import pandas as pd
import matplotlib.pyplot as plt
def sensitivity_analysis_1d(base_value, variable_range, calc_function):
"""
One-dimensional sensitivity analysis
"""
results = []
for value in variable_range:
outcome = calc_function(value)
results.append(outcome)
return results
# Example: NPV sensitivity to discount rate
base_cf = [-1000, 300, 350, 400, 450, 500]
discount_rates = np.linspace(0.05, 0.20, 31)
npv_results = sensitivity_analysis_1d(
0.10,
discount_rates,
lambda r: npv(r, base_cf)
)
# Plot
plt.figure(figsize=(12, 6))
plt.plot(discount_rates * 100, npv_results, linewidth=2, color='#2E86AB')
plt.axhline(y=0, color='red', linestyle='--', linewidth=1, alpha=0.5)
plt.axvline(x=10, color='gray', linestyle='--', linewidth=1, alpha=0.5,
label='Base Case (10%)')
plt.xlabel('Discount Rate (%)', fontsize=12)
plt.ylabel('NPV ($)', fontsize=12)
plt.title('Sensitivity Analysis: NPV vs Discount Rate',
fontsize=16, fontweight='bold', pad=20)
plt.grid(True, alpha=0.3)
plt.legend(fontsize=11)
plt.tight_layout()
plt.show()
# Find break-even discount rate (where NPV = 0)
# This is the IRR
breakeven_rate = irr(base_cf)
print(f"\nBreak-even Discount Rate (IRR): {breakeven_rate*100:.2f}%")
Two-Way Sensitivity Analysis
def sensitivity_analysis_2d(var1_range, var2_range, calc_function):
"""
Two-dimensional sensitivity analysis
"""
results = pd.DataFrame(
index=[f"{v*100:.1f}%" for v in var1_range],
columns=[f"{v*100:.1f}%" for v in var2_range]
)
for i, var1 in enumerate(var1_range):
for j, var2 in enumerate(var2_range):
results.iloc[i, j] = calc_function(var1, var2)
return results.astype(float)
# Example: Stock valuation sensitivity to growth and discount rate
def stock_value(growth, discount):
dividend = 2.50
next_div = dividend * (1 + growth)
if discount <= growth:
return np.nan
return next_div / (discount - growth)
growth_rates = np.linspace(0.03, 0.08, 6)
discount_rates = np.linspace(0.08, 0.13, 6)
sensitivity_table = sensitivity_analysis_2d(
growth_rates,
discount_rates,
stock_value
)
print("\nTwo-Way Sensitivity Analysis")
print("Stock Intrinsic Value")
print("="*70)
print("Growth Rate (rows) vs Discount Rate (columns)")
print(sensitivity_table.to_string())
# Visualize as heatmap
import seaborn as sns
plt.figure(figsize=(10, 8))
sns.heatmap(sensitivity_table, annot=True, fmt='.2f', cmap='RdYlGn',
cbar_kws={'label': 'Stock Value ($)'})
plt.title('Sensitivity Analysis: Stock Value', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Discount Rate', fontsize=12)
plt.ylabel('Growth Rate', fontsize=12)
plt.tight_layout()
plt.show()
8.8 Practice Exercises
Exercise 1: Retirement Planning Calculator
# Your task: Build a comprehensive retirement calculator
# Requirements:
# 1. Calculate how much to save monthly to reach retirement goal
# 2. Account for salary increases over time
# 3. Model different contribution rates (e.g., 401k match)
# 4. Calculate post-retirement income from savings
# 5. Account for inflation
# 6. Create visualization showing savings growth
# 7. Perform sensitivity analysis on return assumptions
Exercise 2: Real Estate Investment Model
# Your task: Build a real estate DCF model
# Requirements:
# 1. Project rental income for 10 years
# 2. Account for vacancy rates and operating expenses
# 3. Include property appreciation
# 4. Calculate cash flows after debt service
# 5. Determine property value at sale (terminal value)
# 6. Calculate IRR and cash-on-cash return
# 7. Compare to alternative investments
Exercise 3: Company Valuation Project
# Your task: Value a real publicly-traded company
# Requirements:
# 1. Download historical financials (use yfinance or manual research)
# 2. Project 5 years of revenue, margins, and cash flows
# 3. Calculate WACC
# 4. Build complete DCF model
# 5. Calculate intrinsic value per share
# 6. Compare to current market price
# 7. Perform sensitivity analysis
# 8. Write investment recommendation
Exercise 4: Portfolio Risk Simulation
# Your task: Build Monte Carlo portfolio simulator
# Requirements:
# 1. Create a portfolio of 5 stocks
# 2. Download historical prices and calculate returns/volatilities
# 3. Calculate correlation matrix
# 4. Run 10,000 simulations of 1-year returns
# 5. Calculate portfolio VaR and CVaR
# 6. Visualize distribution of outcomes
# 7. Calculate probability of achieving target return
# 8. Test different asset allocations
Module 8 Summary
Congratulations! You've mastered financial modeling fundamentals.
What You've Accomplished
Core Concepts
- Time value of money calculations (PV, FV, NPV, IRR)
- Bond pricing and yield analysis
- Stock valuation methods (DDM, multi-stage models)
- Discounted cash flow (DCF) modeling
- Option pricing with Black-Scholes
- Monte Carlo simulation for risk analysis
Modeling Skills
- Building complete financial projection models
- Creating DCF valuations from scratch
- Calculating option Greeks
- Running scenario and sensitivity analyses
- Simulating thousands of possible outcomes
- Automating complex calculations with Python
Practical Applications
- Valuing companies and investments
- Analyzing bonds and fixed income
- Pricing options and derivatives
- Assessing investment risk
- Making data-driven financial decisions
Real-World Capabilities
You can now:
- Value any company using DCF methodology
- Price bonds and calculate yields
- Model investment returns and risks
- Build complete financial models in Python
- Perform professional-grade sensitivity analysis
- Use Monte Carlo simulation for risk assessment
- Make quantitative investment decisions
The Power of Models
Financial models don't predict the future—they help you think about it systematically. The models you've learned are used daily by:
- Investment bankers valuing companies for M&A
- Portfolio managers analyzing potential investments
- Corporate finance teams making capital allocation decisions
- Risk managers assessing exposure
- Financial advisors planning client portfolios
Critical Thinking
Remember: "All models are wrong, but some are useful." Your models are only as good as your assumptions. Always:
- Stress test your assumptions
- Run sensitivity analysis
- Consider multiple scenarios
- Understand model limitations
- Use judgment alongside quantitative analysis
What's Next
In Module 9, we'll explore statistical analysis for finance—hypothesis testing, regression models, time series forecasting, and other statistical techniques that complement your modeling skills.
Before Moving Forward
Ensure you're comfortable with:
- Time value of money calculations
- Building DCF models
- Running Monte Carlo simulations
- Performing sensitivity analysis
- Understanding model assumptions
Practice Recommendations
- Model Real Companies: Practice DCF on actual stocks
- Challenge Assumptions: Always question your inputs
- Compare Models: See how different methods value the same asset
- Build Templates: Create reusable model templates
- Stay Current: Financial modeling evolves; keep learning
The Foundation is Complete
You've now built a comprehensive toolkit:
- Data acquisition and cleaning
- Statistical analysis and visualization
- Portfolio construction and optimization
- Technical and fundamental analysis
- Financial modeling and valuation
You're equipped to perform institutional-quality financial analysis. The final modules will add statistical rigor and bring everything together in a capstone project.
Continue to Module 9: Statistical Analysis for Finance →

