Download income statement, balance sheet, and cash flow data, and clean and format them if needed.
# library imports
import requests
import pandas as pd
import pandas as pd
import numpy as np
import yfinance as yf
# stock symbol
= 'aapl'
ticker
# Mapping each URL to a descriptive sheet name
= {
url_sheet_map f"https://stockanalysis.com/stocks/{ticker}/": "Overview",
f"https://stockanalysis.com/stocks/{ticker}/financials/": "Income Statement",
f"https://stockanalysis.com/stocks/{ticker}/financials/balance-sheet/": "Balance Sheet",
f"https://stockanalysis.com/stocks/{ticker}/financials/cash-flow-statement/": "Cash Flow",
f"https://stockanalysis.com/stocks/{ticker}/financials/ratios/": "Ratios"
}
# Create an Excel writer
with pd.ExcelWriter(f"{ticker}_financial_statements.xlsx") as writer:
# Loop through each URL and its corresponding sheet name
in url_sheet_map.items():
for url, sheet_name f"Processing: {url}")
print(= requests.get(url)
response # Ensure the request was successful
response.raise_for_status()
# Parse all tables from the current URL
= pd.read_html(response.text)
tables f"Found {len(tables)} tables at {url}.")
print(
# If multiple tables, we write them sequentially in the same sheet
= 0 # Initial row position for writing
startrow # Use a list to collect dataframes if you prefer concatenation, but here we write them one after another
in enumerate(tables):
for idx, table # Optionally, add a header row in the Excel sheet to indicate table separation
= pd.DataFrame({f"Table {idx} from {sheet_name}": []})
header =sheet_name, startrow=startrow)
header.to_excel(writer, sheet_name+= 1 # Move down one row for the table data
startrow
# Write the table to the current sheet starting at the designated row
=sheet_name, startrow=startrow)
table.to_excel(writer, sheet_name
# Update the startrow for the next table (current table rows + 2 extra rows as spacer)
+= len(table.index) + 2
startrow
print("All tables have been saved into 'tables_by_url.xlsx', each URL in its own sheet.")
# Parameters
= "AAPL"
TICKER = f"{TICKER}_financial_statements.xlsx"
EXCEL = "FY2024"
FY_COL
def parse_value(val):
if isinstance(val, str):
= val.replace(",", "").strip()
val if val in ['-', '', 'NA', 'N/A']:
return np.nan
if "%" in val:
try:
return float(val.replace("%", "").strip()) / 100
except:
return np.nan
= {'B': 1e9, 'M': 1e6, 'T': 1e12}
m if val[-1] in m:
try:
return float(val[:-1].strip()) * m[val[-1]]
except:
return np.nan
try:
return float(val) * 1e6 if val[-1].isdigit() else np.nan
except:
return np.nan
return np.nan if pd.isna(val) else val
def clean_sheet(sheet, file):
= pd.read_excel(file, sheet_name=sheet, header=None).iloc[4:].reset_index(drop=True)
df if pd.api.types.is_numeric_dtype(df.iloc[:,0]) and (df.iloc[:,0].fillna(-1) == pd.Series(range(len(df)))).all():
= df.iloc[:,1:]
df = df.shape[1]
n if n == 7:
= ["Item", FY_COL, "FY2023", "FY2022", "FY2021", "FY2020", "Notes"]
df.columns elif n == 8:
= ["Item", FY_COL, "FY2023", "FY2022", "FY2021", "FY2020", "Extra", "Notes"]
df.columns else:
= [f"Col{i}" for i in range(n)]
df.columns for c in df.columns:
if c not in ["Item", "Notes"]:
= df[c].apply(parse_value)
df[c] return df
# Load Data
= clean_sheet("Income Statement", EXCEL)
fin = clean_sheet("Balance Sheet", EXCEL).set_index("Item")
bal = clean_sheet("Cash Flow", EXCEL) cf
If not available in the statements, FCFF can be computed using: \[\text{FCFF} = \text{EBIT} \times (1 - t) + \text{Depreciation} - \text{CAPEX} - \Delta WC\]
def get_val(df, key, col=FY_COL, default=None):
= df[df["Item"].str.contains(key, case=False, na=False)]
row return row[col].values[0] if not row.empty else default
# Extract metrics
= get_val(fin, "EBIT|Operating Income")
EBIT = 0.21 # Default effective tax rate
tax = get_val(cf, "Depreciation", default=0)
depr = abs(get_val(cf, "Capital Expenditure", default=0))
capex
# Calculate working capital change if available
if "Working Capital" in bal.index:
= bal.loc["Working Capital"]
wc = wc.iloc[0] - (wc.iloc[1] if len(wc) > 1 else 0)
delta_wc else:
= 0
delta_wc
# Calculate net debt
if "Net Cash (Debt)" in bal.index:
= bal.loc["Net Cash (Debt)", FY_COL]
net_debt else:
= (bal.loc["Total Debt", FY_COL] - bal.loc["Cash & Equivalents", FY_COL]
net_debt if "Total Debt" in bal.index and "Cash & Equivalents" in bal.index else 0)
= get_val(cf, "Free Cash Flow", EBIT * (1 - tax) + depr - capex - delta_wc) FCFF0
Forecast the FCFF over a set period using an assumed growth rate.
The forecast period is typically 5–10 years.
\[\text{FCFF}_t = \text{FCFF}_0 \times (1 + g)^t\]
# Forecasting FCFF
= 0.15 # 15% growth assumption
DEFAULT_GROWTH = 5
FORECAST_YEARS
= DEFAULT_GROWTH
growth = [FCFF0 * (1 + growth) ** t for t in range(1, FORECAST_YEARS + 1)] if FCFF0 else [None] * FORECAST_YEARS forecast
Calculate the cost of equity using the CAPM model.
Combine the cost of equity and cost of debt (after tax) using their respective weights to determine the WACC.
Cost of Equity (CAPM): \[r_e = r_f + \beta (r_m - r_f)\] where:
\(r_f\) = risk-free rate,
\(\beta\) = beta of the stock,
\(r_m - r_f\) = market risk premium.
WACC:
\[WACC = w_e \times r_e + w_d \times r_d \times (1 - t)\]
where:
\(w_e\) and \(w_d\) are the weights of equity and debt,
\(r_d\) is the cost of debt,
\(t\) is the tax rate.
# WACC Calculation
# Retrieve beta
= yf.Ticker(TICKER).info
info = info.get("beta", 1.0)
beta = info.get("sharesOutstanding", None)
shares
# Risk-free rate from TLT
= yf.Ticker("TLT").info.get("yield", 0.022)
rf = yf.Ticker("SPY").history(period="20y")["Close"].resample("Y").last()
spy_hist if len(spy_hist) >= 2:
= (spy_hist.iloc[-1] / spy_hist.iloc[0])**(1 / (len(spy_hist)-1)) - 1
spy_cagr else:
= 0.08
spy_cagr = spy_cagr - rf
mrp # Cost of Equity
= rf + beta * mrp
ce # Cost of Debt
= DEFAULT_COST_DEBT # Assumed cost of debt
de
# Determine market value weights
= shares * price if shares and price else None
market_equity = bal.loc["Total Debt", FY_COL] if "Total Debt" in bal.index else 0
market_debt if market_equity and (market_equity + market_debt) > 0:
= market_equity / (market_equity + market_debt)
we = market_debt / (market_equity + market_debt)
wd else:
= 1, 0
we, wd
= we * ce + wd * de * (1 - tax) WACC
Terminal Value: Estimate the value of the business beyond the forecast period.
DCF Calculation: Discount the forecasted FCFFs and terminal value back to the present value using WACC.
Terminal Value Equation: \[TV = \frac{\text{FCFF}_n \times (1 + g_{term})}{WACC - g_{term}}\]
where \(g_{term}\) is the terminal growth rate.
Enterprise Value (EV): \[EV = \sum_{t=1}^{n} \frac{\text{FCFF}_t}{(1 + WACC)^t} + \frac{TV}{(1 + WACC)^n}\]
Equity Value & Intrinsic Price:
\[\text{Equity Value} = EV - \text{Net Debt} \]\[\text{Intrinsic Price} = \frac{\text{Equity Value}}{\text{Shares Outstanding}}\]
# Terminal Value and DCF Valuation
= 0.04 # Terminal growth rate assumption
TERM_GROWTH
# Discount forecasted FCFFs
= [f / ((1 + WACC) ** t) for t, f in enumerate(forecast, start=1)]
disc_FCFF
# Calculate Terminal Value using the last year's FCFF
= forecast[-1] * (1 + TERM_GROWTH) / (WACC - TERM_GROWTH)
term_val = term_val / ((1 + WACC) ** FORECAST_YEARS)
disc_term
# Enterprise Value (EV)
= sum(disc_FCFF) + disc_term
EV
# Equity Value and Intrinsic Share Price
= EV - net_debt
eq_val = eq_val / shares if shares else None
intrinsic
= yf.Ticker(TICKER).info
info = info.get("currentPrice", None)
price print(f"Intrinsic Price: {intrinsic:,.2f}, Current Price: {price:,.2f}")
By following these steps and using the code snippets provided, you can build a basic DCF valuation model. Adjust the assumptions and refine the code as needed to fit your data and analysis requirements.