Quantitative Value Strategy
Quantitative value intestment means invest in the stocks that are “cheapest” relative to common measures of business value (like earnings or assets), which focuses on identifying undervalued assets and purchasing them at a price below their intrinsic value (true worth), this strategy is a cornerstone of successful long-term investing.
- Intrinsic value: the price level that reflects the actual worth or value of the company
- Margin of safety: the difference between purchase value and the true value
Quantitative value investing relies on various financial ratios to assess the value of potential investments, here below are the commonly used ratios in quantitative value investing.
- Price-to-Earnings (P/E) Ratio: a lower P/E ratio is often seen as an indicator of undervaluation.
- Return on Equity (ROE): a higher ROE is generally desirable.
- Price-to-book (P/B) Ratio: a P/B ratio below 1 can suggest that a stock is undervalued.
- Price-to-Sales (P/S) Ratio: a lower P/S ratio can indicate undervaluation.
- Price-to-Cash Flow (P/CF) Ratio: a lower P/CF ratio may indicate that the stock is undervalued. It reflects how much cash the company generates relative to its stock price.
- Dividend Yield: it measures the annual dividends paid by a company as a percentage of its stock price, therefore a higher dividend yield may be attractive to income-focused investors.
- Earnings Yield: it is the inverse of the P/E ratio. It’s a measure of how much a company earns in relation to its stock price.
- Debt-to-Equity (D/E) Ratio: a lower D/E ratio indicates lower financial risk.
- Current Ratio: it measures a company’s ability to cover its short-term liabilities with its short-term assets. A higher current ratio is often preferred.
- Free Cash Flow (FCF) Yield: it compares a company’s free cash flow to its market capitalisation. A higher FCF yield can indicate value and financial health.
- Enterprise Value divided by Earnings Before Interest, Taxes, Depreciation, and Amortization (EV/EBITDA), it takes into account a company’s debt and cash levels in addition to its stock price and relates that value to the firm’s cash profitability. Higher enterprise multiples are expected in high-growth industries and lower multiples in industries with slow growth.
- Enterprise Value divided by Gross Profit (EV/GP), it is a profitability financial ratio that estimates the enterprise value of a company to its gross profit. Generally, the lower the ratio, the lower is the company’s net worth.
The goal of this section is trying to create an investing strategy following the instruction on https://github.com/nickmccullum/algorithmic-trading-python/tree/master, with this strategy it selects the 50 stocks in S&P 600 list with the highest value metric (we start with P/E ratio). From there, the trades with an equal-weight portfolio of these 50 stocks will be calculated.
Load the list of S&P 600 companies from Wikepedia
The S&P 600 is an index of small-cap company stocks created by Standard & Poor’s, selected by a committee based on recent profitability and other factors.
Currently there are 3 S&P Small Cap 600 ETFs traded on the U.S. markets, the largest one is iShares S&P Small-Cap 600 Value ETF IJS with around 7.09 Billon USD in assets.
import pandas as pd
def load_data(url):
html = pd.read_html(url, header=0)
return html
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_600_companies'
df = load_data(url)[0]
df.head()
| Symbol | Company | GICS Sector | GICS Sub-Industry | Headquarters Location | SEC filings | CIK |
---|
0 | AAON | AAON, Inc. | Industrials | Building Products | Tulsa, Oklahoma | view | 824142 |
---|
1 | AAP | Advance Auto Parts, Inc. | Consumer Discretionary | Automotive Retail | Raleigh, North Carolina | view | 1158449 |
---|
2 | AAT | American Assets Trust | Real Estate | Diversified REITs | San Diego, California | view | 1500217 |
---|
3 | ABCB | Ameris Bancorp | Financials | Regional Banks | Atlanta, Georgia | view | 351569 |
---|
4 | ABG | Asbury Automotive Group | Consumer Discretionary | Automotive Retail | Duluth, Georgia | view | 1144980 |
---|
Retrieve the latest stock info using yfinance
import yfinance as yf
def retrieveQuote(ticker):
stock_dict = yf.Ticker(ticker).info
stock = pd.DataFrame.from_dict(stock_dict, orient='index')
stock = stock.reset_index()
stock = stock.rename(columns={'index': 'Item', 0: 'Value'})
return stock
P/E Ratio
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'trailingPE']
| Item | Value |
---|
40 | trailingPE | 41.000004 |
---|
Return on Equity (ROE)
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] =='returnOnEquity']
| Item | Value |
---|
122 | returnOnEquity | 0.28097 |
---|
P/B Ratio
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'priceToBook']
| Item | Value |
---|
70 | priceToBook | 10.003571 |
---|
P/S Ratio
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'priceToSalesTrailing12Months']
| Item | Value |
---|
54 | priceToSalesTrailing12Months | 6.116228 |
---|
Price-to-Cash Flow (P/CF) Ratio
price = yf.Ticker('AAON').history(period='1d')['Close'].item()
price
84.05000305175781
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'totalCashPerShare']
| Item | Value |
---|
113 | totalCashPerShare | 0.035 |
---|
print('Price-to-Cash Flow (P/CF) Ratio for AAON is: ', price/retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'totalCashPerShare']['Value'].item())
Price-to-Cash Flow (P/CF) Ratio for AAON is: 2401.4286586216517
EV/EBITDA
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'enterpriseToEbitda']
| Item | Value |
---|
87 | enterpriseToEbitda | 26.208 |
---|
Dividend Yield
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'dividendYield']
| Item | Value |
---|
35 | dividendYield | 0.0038 |
---|
Debt-to-Equity (D/E) Ratio
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'debtToEquity']
| Item | Value |
---|
119 | debtToEquity | 8.581 |
---|
Current Ratio
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'currentRatio']
| Item | Value |
---|
117 | currentRatio | 3.027 |
---|
Free Cash Flow (FCF) Yield
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'freeCashflow']
| Item | Value |
---|
123 | freeCashflow | 16578000 |
---|
retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'marketCap']
| Item | Value |
---|
51 | marketCap | 6828633600 |
---|
print("FCF Yield for AAON is: ", retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'freeCashflow']['Value'].item() / retrieveQuote('AAON')[retrieveQuote('AAON')['Item'] == 'marketCap']['Value'].item())
FCF Yield for AAON is: 0.002427718482362269
Retrieve the latest stock price using yfinance
We retrieve the latest day’s stock price using yfinance.
import yfinance as yf
import warnings
warnings.filterwarnings('ignore')
data = yf.download(
tickers = list(df['Symbol']),
period = '1d',
interval = '1d',
group_by = 'ticker',
auto_adjust = True,
prepost = True,
threads = True,
proxy = None
)
[*********************100%%**********************] 602 of 602 completed
2 Failed downloads:
['MOG.A', 'CWEN.A']: Exception('%ticker%: No data found, symbol may be delisted')
Calculate PE ratio and remove the highest PE ratio stocks
tickers_unavailable = ['MOG.A', 'CWEN.A']
tickers = [ticker for ticker in list(df['Symbol']) if ticker not in tickers_unavailable]
print('Number of available tickers: ', len(tickers))
Number of available tickers: 600
my_columns = ['Ticker', 'Price', 'Price-to-Earnings Ratio', 'Number of Shares to Buy']
final_dataframe = pd.DataFrame(columns = my_columns)
final_dataframe
| Ticker | Price | Price-to-Earnings Ratio | Number of Shares to Buy |
---|
portfolio_size = input("Enter the value of your portfolio:")
try:
val = float(portfolio_size)
except ValueError:
print("That's not a number! \n Try again:")
portfolio_size = input("Enter the value of your portfolio:")
Enter the value of your portfolio:1000000
for i in range(0,len(tickers)):
ticker = tickers[i]
trailingPE = retrieveQuote(ticker)[retrieveQuote(ticker)['Item'] == 'trailingPE']
if trailingPE.shape[0] == 1:
pe_ratio = retrieveQuote(ticker)[retrieveQuote(ticker)['Item'] == 'trailingPE']['Value'].item()
else:
pe_ratio = "NA"
final_dataframe = final_dataframe.append(
pd.Series(
[
ticker,
data[ticker]['Close'].item(),
pe_ratio,
'NA'
],
index = my_columns),
ignore_index = True)
final_dataframe.head()
| Ticker | Price | Price-to-Earnings Ratio | Number of Shares to Buy |
---|
0 | AAON | 84.050003 | 41.000004 | NA |
---|
1 | AAP | 61.099998 | 17.761627 | NA |
---|
2 | AAT | 21.530001 | 25.630955 | NA |
---|
3 | ABCB | 45.880001 | 11.794345 | NA |
---|
4 | ABG | 214.619995 | 7.467641 | NA |
---|
final_dataframe = final_dataframe[final_dataframe['Price-to-Earnings Ratio'] != 'NA']
final_dataframe['Price-to-Earnings Ratio'] = final_dataframe['Price-to-Earnings Ratio'].astype(float)
final_dataframe.sort_values('Price-to-Earnings Ratio', ascending=True, inplace=True)
final_dataframe = final_dataframe[:50]
final_dataframe.reset_index(drop=True, inplace=True)
final_dataframe.head()
| Ticker | Price | Price-to-Earnings Ratio | Number of Shares to Buy |
---|
0 | VTLE | 46.610001 | 1.393840 | NA |
---|
1 | BFH | 36.779999 | 2.495251 | NA |
---|
2 | AMCX | 12.430000 | 2.536735 | NA |
---|
3 | CPE | 31.280001 | 3.039845 | NA |
---|
4 | CCOI | 78.790001 | 3.492465 | NA |
---|
import math
position_size = float(portfolio_size) / len(tickers)
for i in range(0,len(final_dataframe['Ticker'])):
final_dataframe.loc[i, 'Number of Shares to Buy'] = math.floor(position_size / final_dataframe['Price'][i])
final_dataframe.head()
| Ticker | Price | Price-to-Earnings Ratio | Number of Shares to Buy |
---|
0 | VTLE | 46.610001 | 1.393840 | 35 |
---|
1 | BFH | 36.779999 | 2.495251 | 45 |
---|
2 | AMCX | 12.430000 | 2.536735 | 134 |
---|
3 | CPE | 31.280001 | 3.039845 | 53 |
---|
4 | CCOI | 78.790001 | 3.492465 | 21 |
---|
Save the output in Excel using XlsxWriter
import xlsxwriter
writer = pd.ExcelWriter('quantitative value.xlsx', engine = 'xlsxwriter')
final_dataframe.to_excel(writer, 'Recommended Trades', index = False)
- String format for tickers
- $xx.xx format for stock prices
- $xx,xxx for market capitalization
- Integer format for the number of shares to buy
background_color = '#0a0a23'
font_color = '#ffffff'
string_format = writer.book.add_format(
{
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
dollar_format = writer.book.add_format(
{
'num_format':'$0.00',
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
capital_format = writer.book.add_format(
{
'num_format':'$#,##0',
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
integer_format = writer.book.add_format(
{
'num_format':'0',
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
float_format = writer.book.add_format(
{
'num_format':'0.000',
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
percent_format = writer.book.add_format(
{
'num_format':'0.0%',
'font_color': font_color,
'bg_color': background_color,
'border': 1
}
)
column_formats = {
'A': ['Ticker', string_format],
'B': ['Stock Price', dollar_format],
'C': ['Price-to-Earnings Ratio', float_format],
'D': ['Number of Shares to Buy', integer_format],
}
for column in column_formats.keys():
writer.sheets['Recommended Trades'].set_column(f'{column}:{column}', 18, column_formats[column][1])
writer.sheets['Recommended Trades'].write(f'{column}1', column_formats[column][0], string_format)
writer.save()