Skip to content

Instantly share code, notes, and snippets.

@nreHieW
Last active September 29, 2024 15:53
Show Gist options
  • Save nreHieW/6365cb92523f0d347c1338d22f74f780 to your computer and use it in GitHub Desktop.
Save nreHieW/6365cb92523f0d347c1338d22f74f780 to your computer and use it in GitHub Desktop.
Discounted Cash Flow (DCF) modelling in Python
import pandas as pd
import numpy as np
def calc_cost_of_capital(interest_expense, pre_tax_cost_of_debt, average_maturity, bv_debt, num_shares_outstanding, curr_price, unlevered_beta, tax_rate, risk_free_rate, equity_risk_premium):
market_value_of_debt = interest_expense * (1 - 1 / ((1 + pre_tax_cost_of_debt) ** average_maturity)) / pre_tax_cost_of_debt + bv_debt / ((1 + pre_tax_cost_of_debt) ** average_maturity)
market_value_of_equity = num_shares_outstanding * curr_price
market_value_of_capital = market_value_of_debt + market_value_of_equity
equity_weight = market_value_of_equity / market_value_of_capital
debt_weight = market_value_of_debt / market_value_of_capital
levered_beta = unlevered_beta * (1 + (1 - tax_rate) * (market_value_of_debt / market_value_of_equity))
cost_of_debt = pre_tax_cost_of_debt * (1 - tax_rate)
cost_of_equity = risk_free_rate + levered_beta * equity_risk_premium
cost_of_capital = cost_of_debt * debt_weight + cost_of_equity * equity_weight
return cost_of_capital, {
"cost_of_debt": cost_of_debt,
"cost_of_equity": cost_of_equity,
"levered_beta": levered_beta,
"risk_free_rate": risk_free_rate,
"equity_risk_premium": equity_risk_premium,
}
def r_and_d_adjustment(expenses: list):
num_years = len(expenses) - 1
expenses = np.array(expenses)
unamortized_amount = np.linspace(1, 0, num_years + 1)
unamortized_amount = unamortized_amount * expenses
unamortized_amount = np.sum(unamortized_amount)
amortization_this_year = expenses[1:] / num_years
amortization_this_year = np.sum(amortization_this_year)
adjustment = expenses[0] - amortization_this_year
return adjustment, unamortized_amount
def dcf(
revenues,
operating_income,
interest_expense,
book_value_of_equity,
book_value_of_debt,
cash_and_marketable_securities,
cross_holdings_and_other_non_operating_assets,
minority_interest,
number_of_shares_outstanding,
curr_price,
effective_tax_rate,
marginal_tax_rate,
unlevered_beta,
risk_free_rate,
equity_risk_premium,
mature_erp,
pre_tax_cost_of_debt,
average_maturity,
prob_of_failure,
value_of_options,
revenue_growth_rate_next_year,
operating_margin_next_year,
compounded_annual_revenue_growth_rate,
target_pre_tax_operating_margin,
year_of_convergence_for_margin,
years_of_high_growth,
sales_to_capital_ratio_early,
sales_to_capital_ratio_steady,
r_and_d_expenses,
discount_rate: None,
):
start_cost_of_capital, cost_of_capital_components = calc_cost_of_capital(
interest_expense, pre_tax_cost_of_debt, average_maturity, book_value_of_debt, number_of_shares_outstanding, curr_price, unlevered_beta, marginal_tax_rate, risk_free_rate, equity_risk_premium
)
if discount_rate is not None:
start_cost_of_capital = discount_rate
if len(r_and_d_expenses) > 0:
r_and_d_adjustment_value, value_of_research_asset = r_and_d_adjustment(r_and_d_expenses)
operating_income += r_and_d_adjustment_value
else:
value_of_research_asset = 0
revenue_growth_rates = (
[0] + [revenue_growth_rate_next_year] + [compounded_annual_revenue_growth_rate] * (years_of_high_growth - 1) + np.linspace(compounded_annual_revenue_growth_rate, risk_free_rate, 10 - years_of_high_growth).tolist() + [risk_free_rate]
)
df = pd.DataFrame({"revenue_growth_rate": revenue_growth_rates})
df["revenues"] = revenues * (1 + df["revenue_growth_rate"]).cumprod()
starting_operating_margin = operating_income / revenues
# TODO: Adjust operating income then get margin rather than make it an input
df["operating_margin"] = (
[starting_operating_margin] + np.linspace(operating_margin_next_year, target_pre_tax_operating_margin, year_of_convergence_for_margin).tolist() + [target_pre_tax_operating_margin] * (11 - year_of_convergence_for_margin)
)
df["operating_income"] = df["revenues"] * df["operating_margin"]
df["tax_rate"] = [effective_tax_rate] * 6 + np.linspace(effective_tax_rate, marginal_tax_rate, 5).tolist() + [marginal_tax_rate]
df["taxes"] = np.where(df["operating_income"] > 0, df["operating_income"] * df["tax_rate"], 0)
df["nol"] = np.where(df["operating_income"] < 0, -df["operating_income"] * 0.8, 0) # https://www.investopedia.com/terms/t/tax-loss-carryforward.asp
df["nol_cumulative"] = df["nol"].cumsum()
df["nol_utilized"] = np.where(df["operating_income"] > 0, np.minimum(df["nol_cumulative"], df["operating_income"]), 0)
df["nol_cumulative"] -= df["nol_utilized"]
df["taxes"] -= df["nol_utilized"] * df["tax_rate"]
df["ebit_after_tax"] = df["operating_income"] - df["taxes"]
df["reinvestment"] = (df["revenues"].diff(-1) * -1) / (np.linspace(sales_to_capital_ratio_early, sales_to_capital_ratio_steady, 7).tolist() + [sales_to_capital_ratio_steady] * 5)
df.loc[0, "reinvestment"] = 0
starting_invested_capital = book_value_of_equity + book_value_of_debt - cash_and_marketable_securities + value_of_research_asset
df["invested_capital"] = starting_invested_capital + df["reinvestment"].cumsum()
df["roic"] = df["ebit_after_tax"] / df["invested_capital"]
end_cost_of_capital = risk_free_rate + mature_erp
df["cost_of_capital"] = [start_cost_of_capital] * 6 + np.linspace(start_cost_of_capital, end_cost_of_capital, 6).tolist()
df.loc[11, "roic"] = end_cost_of_capital
df.loc[11, "reinvestment"] = risk_free_rate / df.loc[11, "roic"] * df.loc[11, "ebit_after_tax"]
df["fcff"] = df["ebit_after_tax"] - df["reinvestment"]
df["discount_factor"] = (1 / (1 + df["cost_of_capital"])).cumprod()
df["discount_factor"] = df["discount_factor"].shift(1)
df["pv_fcff"] = df["fcff"] * df["discount_factor"]
terminal_val = df.loc[11, "fcff"] / (end_cost_of_capital - risk_free_rate)
terminal_pv = terminal_val * df.loc[10, "discount_factor"]
pv_cf = df.loc[1:11, "pv_fcff"].sum() + terminal_pv
proceeds_if_fail = pv_cf * 0.5
op_value = pv_cf * (1 - prob_of_failure) + proceeds_if_fail * prob_of_failure
value_of_equity = op_value - book_value_of_debt - minority_interest + cash_and_marketable_securities + cross_holdings_and_other_non_operating_assets
value_of_equity = value_of_equity - value_of_options
value_per_share = value_of_equity / number_of_shares_outstanding
df.index = ["Base"] + list(range(1, 11)) + ["Terminal"]
return (
value_per_share,
df,
cost_of_capital_components,
{
"present_value_of_cash_flows": pv_cf,
"book_value_of_debt": book_value_of_debt,
"cash_and_marketable_securities": cash_and_marketable_securities,
"cross_holdings_and_other_non_operating_assets": cross_holdings_and_other_non_operating_assets,
"minority_interest": minority_interest,
},
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment