Skip to content

Instantly share code, notes, and snippets.

@prateekmalhotra
Last active April 20, 2024 14:23
Show Gist options
  • Save prateekmalhotra/431ebfa2d3168f7b7dbe7a86f17e3f2f to your computer and use it in GitHub Desktop.
Save prateekmalhotra/431ebfa2d3168f7b7dbe7a86f17e3f2f to your computer and use it in GitHub Desktop.
Estimate the fair value of your stonks using this DCF model. Based on: https://www.buffettsbooks.com/how-to-invest-in-stocks/advanced-course/lesson-35/. Also, added a formula to understand how much growth is priced-in (Page 295, The Intelligent Investor).
from lxml import html
import requests
import json
import argparse
from collections import OrderedDict
import warnings
warnings.filterwarnings('ignore')
def parse(ticker):
url = "https://stockanalysis.com/stocks/{}/financials/cash-flow-statement".format(ticker)
response = requests.get(url, verify=False)
parser = html.fromstring(response.content)
fcfs = parser.xpath('//table[contains(@id,"financial-table")]//tr[td/span/text()[contains(., "Free Cash Flow")]]')[0].xpath('.//td/span/text()')[1:]
last_fcf = float(fcfs[0].replace(',', ''))
url = "https://finance.yahoo.com/quote/{}/analysis?p={}".format(ticker, ticker)
response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0'})
parser = html.fromstring(response.content)
ge = parser.xpath('//table//tbody//tr')
for row in ge:
label = row.xpath("td/span/text()")[0]
if 'Next 5 Years' in label:
try:
ge = float(row.xpath("td/text()")[0].replace('%', ''))
except:
ge = []
break
url = "https://stockanalysis.com/stocks/{}/".format(ticker)
response = requests.get(url, verify=False)
parser = html.fromstring(response.content)
shares = parser.xpath('//div[@class="order-1 flex flex-row gap-4"]//table//tbody//tr[td/text()[contains(., "Shares Out")]]')
shares = shares[0].xpath('td/text()')[1]
factor = 1000 if 'B' in shares else 1
shares = float(shares.replace('B', '').replace('M', '')) * factor
url = "https://stockanalysis.com/stocks/{}/financials/".format(ticker)
response = requests.get(url, verify=False)
parser = html.fromstring(response.content)
eps = parser.xpath('//table[contains(@id,"financial-table")]//tr[td/span/text()[contains(., "EPS (Diluted)")]]')[0].xpath('.//td/span/text()')[1:]
eps = float(eps[0].replace(",", ""))
market_price = float(parser.xpath('//div[@class="price-ext"]/text()')[0].replace('$', '').replace(',', ''))
return {'fcf': last_fcf, 'ge': ge, 'yr': 5, 'dr': 10, 'pr': 2.5, 'shares': shares, 'eps': eps, 'mp': market_price}
def dcf(data):
forecast = [data['fcf']]
if data['ge'] == []:
raise ValueError("No growth rate available from Yahoo Finance")
for i in range(1, data['yr']):
forecast.append(round(forecast[-1] + (data['ge'] / 100) * forecast[-1], 2))
forecast.append(round(forecast[-1] * (1 + (data['pr'] / 100)) / (data['dr'] / 100 - data['pr'] / 100), 2)) #terminal value
discount_factors = [1 / (1 + (data['dr'] / 100))**(i + 1) for i in range(len(forecast) - 1)]
pvs = [round(f * d, 2) for f, d in zip(forecast[:-1], discount_factors)]
pvs.append(round(discount_factors[-1] * forecast[-1], 2)) # discounted terminal value
print("Forecasted cash flows: {}".format(", ".join(map(str, forecast))))
print("PV of cash flows: {}".format(", ".join(map(str, pvs))))
dcf = sum(pvs)
print("Fair value: {}\n".format(dcf / data['shares']))
def reverse_dcf(data):
pass
def graham(data):
if data['eps'] > 0:
expected_value = data['eps'] * (8.5 + 2 * (data['ge']))
ge_priced_in = (data['mp'] / data['eps'] - 8.5) / 2
print("Expected value based on growth rate: {}".format(expected_value))
print("Growth rate priced in for next 7-10 years: {}\n".format(ge_priced_in))
else:
print("Not applicable since EPS is negative.")
if __name__ == "__main__":
argparser = argparse.ArgumentParser()
argparser.add_argument('ticker', help='Ticker to analyse. Example: GOOG')
argparser.add_argument('--discount_rate', help='Discount rate in %. Default: 10', default=10)
argparser.add_argument('--growth_estimate', help='Estimated yoy growth rate. Default: Fetched from Yahoo Finance')
argparser.add_argument('--terminal_rate', help='Terminal growth rate. Default: 2.5')
argparser.add_argument('--period', help='Time period in years. Default: 5')
args = argparser.parse_args()
ticker = args.ticker
print("Fetching data for %s...\n" % (ticker))
data = parse(ticker)
print("=" * 80)
print("DCF model (basic)")
print("=" * 80 + "\n")
if args.period is not None:
data['yr'] = int(args.period)
if args.growth_estimate is not None:
data['ge'] = float(args.growth_estimate)
if args.discount_rate is not None:
data['dr'] = float(args.discount_rate)
if args.terminal_rate is not None:
data['pr'] = float(args.terminal_rate)
print("Market price: {}".format(data['mp']))
print("EPS: {}".format(data['eps']))
print("Growth estimate: {}".format(data['ge']))
print("Term: {} years".format(data['yr']))
print("Discount Rate: {}%".format(data['dr']))
print("Perpetual Rate: {}%\n".format(data['pr']))
dcf(data)
print("=" * 80)
print("Graham style valuation basic (Page 295, The Intelligent Investor)")
print("=" * 80 + "\n")
graham(data)
@dorian1142
Copy link

good work, thank you!

@prateekmalhotra
Copy link
Author

good work, thank you!

Ofcourse! Let me know if you have any troubles / suggestions.

@anyfactor
Copy link

Thank you for your work.

I have never read up on value investment strategies. Boomer stocks like GE, F, GM are resulting in fair values that are several times higher than their respective market price, while hype/rallying stocks of last year such as PLTR, TDOC are resulting in negative fair value.

Not sure of the utility of this type of valuation.

image

@prateekmalhotra
Copy link
Author

Thank you for your work.

I have never read up on value investment strategies. Boomer stocks like GE, F, GM are resulting in fair values that are several times higher than their respective market price, while hype/rallying stocks of last year such as PLTR, TDOC are resulting in negative fair value.

Not sure of the utility of this type of valuation.

Hi there! Thank you for trying the tool out. Couple of things to note to get the most out of this:

  • Make sure that the company you're trying to value actually makes money. Hence, it's not a very good way to understand the value of growth stocks which probably won't generate a profit for another 3-5 years. There's ways to do this, but it's out of the scope of this tool (as of now).
  • This only takes into account the cash flow of a company. Hence, F & GM look good on paper. These companies, however, have a good amount of debt which this method never takes into account. As a general rule, make sure that the Total assets > 1.5 times the total debt before completely relying on the generated result.

@prateekmalhotra
Copy link
Author

To run, python3 value_estimator.py <ticker_name>

Added some parameters for your customization. Optional usage now looks like:
python3 -B value_estimator.py <ticker_name> --period 10 --growth_estimate 12 --discount_rate 10 --terminal_rate 3

Note: All the above parameters are optional and the code will work the same way if you choose not to use this.

@Haedes277
Copy link

I tried to fetch the data for TSLA. IndexError: list index out of range erre is happening. Could you please suggest me how to solve it? thank you, Sir.
Screenshot (312)

@TheSnoozer
Copy link

To make it work, change line 65

- market_price = float(parser.xpath('//div[@class="price-ext"]/text()')[0].replace('$', '').replace(',', ''))
+ market_price = float(parser.xpath('//div[@class="p"]/text()')[0].replace('$', '').replace(',', ''))

(e.g. replace "price-ext" with "p")

@brianhuebner
Copy link

I am getting a list index out of range error. Can you provide any help with this?

image

@Cashiuus
Copy link

Updates to the xpath lines:

fcfs = parser.xpath('//table[contains(@data-test,"financials")]//tr[td/span/text()[contains(., "Free Cash Flow")]]')[0].xpath('.//td/text()')[1:]
shares = parser.xpath('//table[@data-test="overview-info"]//tbody//tr[td/text()[contains(., "Shares Out")]]')
shares = shares[0].xpath('td/text()')[2]
eps = parser.xpath('//table[contains(@data-test,"financials")]//tr[td/span/text()[contains(., "EPS (Diluted)")]]')[0].xpath('.//td/text()')[1:]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment