import yfinance as yf
import pandas as pd
import numpy as np
from pathlib import Path
The acronym FANGMANT stands for Facebook, Apple, Netflix, Google, Microsoft, Amazon, Nvidia and Tesla. Large, highly profitable US tech companies that dominate their respective markets. Other common acronyms are FANG, FAANG and FANGMAN.
In this article, I’m analyzing their stock performance from 2016 to 2021.
Disclaimer: This article is not financial advice. It’s a data analysis for fun.
Download with yfinance
yfinance is a Python package that downloads financial data from Yahoo! Finance. It does not require an API key or other authentication. It’s meant for personal use and research.
= [
tickers "FB", # Meta / Facebook
"AAPL", # Apple
"NFLX", # Netflix
"GOOG", # Alphabet / Google
"MSFT", # Microsoft
"AMZN", # Amazon
"NVDA", # Nvidia
"TSLA", # Tesla,
"URTH" # MSCI World
]
Download historical prices from Yahoo! Finance by Ticker Symbol.
In addition to the FANGMANT Tickers, I added URTH for the MSCI World, a broad index ETF that includes more than 1600 individual stocks from 23 developed countries. It also includes the FANGMANT stocks, along with the stocks of the companies with the highest market capitalization across all industries.
The data is saved as a pickled pandas data frame, so it doesn’t have to be downloaded again. The pickle format maintains the index and multi-level structure of the data frame, which would be lost in the CSV format.
# Save as a file to avoid having to download again
= Path("data.pkl")
data_path
if data_path.is_file():
= pd.read_pickle(data_path)
data_imported else:
= yf.download(
data_imported = " ".join(tickers),
tickers = "5y",
period = "1d",
interval = "ticker",
group_by = True,
auto_adjust = False,
prepost = True,
threads = None
proxy
)
data_imported.to_pickle(data_path)
[ 0%% ]
[*********** 22%% ] 2 of 9 completed
[**************** 33%% ] 3 of 9 completed
[********************* 44%% ] 4 of 9 completed
[**********************56%%* ] 5 of 9 completed
[**********************67%%****** ] 6 of 9 completed
[**********************78%%*********** ] 7 of 9 completed
[**********************89%%***************** ] 8 of 9 completed
[*********************100%%**********************] 9 of 9 completed
1 Failed download:
['FB']: Exception('%ticker%: No data found, symbol may be delisted')
data_imported
NFLX ... NVDA
Open High ... Close Volume
Date ...
2018-10-22 333.100006 335.799988 ... 57.303703 36884400
2018-10-23 318.000000 336.579987 ... 54.785725 62643600
2018-10-24 332.279999 333.000000 ... 49.420174 88428800
2018-10-25 307.119995 319.940002 ... 51.509388 95172000
2018-10-26 300.510010 313.989990 ... 49.142593 66478400
... ... ... ... ... ...
2023-10-16 356.209991 363.079987 ... 460.950012 37509900
2023-10-17 361.100006 362.700012 ... 439.380005 81233300
2023-10-18 351.000000 354.790009 ... 421.959991 62729400
2023-10-19 404.739990 408.950012 ... 421.010010 50123300
2023-10-20 405.630005 410.640015 ... 413.869995 47638100
[1258 rows x 46 columns]
The pandas data frame has multi-level columns. Each ticker symbol (FB, AAPL, …) is a column which has the Open, High, Low and Close as sub-columns. This data structure is hard to work with. StackOverflow conveniently has an answer to the exact issue. I went with the option of turning the wide data frame into a long data frame with a Ticker column.
= data_imported.stack(level=0).rename_axis(["Date", "Ticker"]).reset_index(level=1)
data data
Ticker Adj Close Close ... Low Open Volume
Date ...
2018-10-22 AAPL NaN 52.830986 ... 52.421557 52.625073 115168400.0
2018-10-22 AMZN NaN 89.464996 ... 87.800003 89.199997 90000000.0
2018-10-22 GOOG NaN 55.057999 ... 54.549999 55.153000 30284000.0
2018-10-22 MSFT NaN 103.866936 ... 102.550006 103.573234 26545600.0
2018-10-22 NFLX NaN 329.540009 ... 320.339996 333.100006 17097200.0
... ... ... ... ... ... ... ...
2023-10-20 MSFT NaN 326.670013 ... 325.450012 331.720001 25012600.0
2023-10-20 NFLX NaN 400.959991 ... 398.010010 405.630005 12768900.0
2023-10-20 NVDA NaN 413.869995 ... 410.779999 418.899994 47638100.0
2023-10-20 TSLA NaN 211.990005 ... 210.419998 217.009995 137734000.0
2023-10-20 URTH NaN 117.650002 ... 117.599998 118.830002 285800.0
[10064 rows x 7 columns]
The stack method puts first level (0) column names into the index. This pivots the data frame, going from a wide format (one row per day) to a long format (one row per day per ticker). rename_axis gives names to the index columns. reset_index changes the custom index with two columns (Date and Ticker) to the default index, which is a DatetimeIndex on the Date column.
data.index
DatetimeIndex(['2018-10-22', '2018-10-22', '2018-10-22', '2018-10-22',
'2018-10-22', '2018-10-22', '2018-10-22', '2018-10-22',
'2018-10-23', '2018-10-23',
...
'2023-10-19', '2023-10-19', '2023-10-20', '2023-10-20',
'2023-10-20', '2023-10-20', '2023-10-20', '2023-10-20',
'2023-10-20', '2023-10-20'],
dtype='datetime64[ns]', name='Date', length=10064, freq=None)
= data.index[0]
start_time = data.index[-1] end_time
The first data point is at 2018-10-22 02:00:00 and the last one is at 2023-10-20 02:00:00.
Returns
Let’s calculate the returns of each stock for each year. I’ll use the Close prices for each day. I’ll also add the volatility as a measure of investment risk.
def growth(series: pd.Series) -> float:
return series[-1] / series[0] - 1
def volatility(series: pd.Series, n_days: int = 252) -> pd.Series:
= np.log(series / series.shift(-1))
returns = np.std(returns)
daily_std = daily_std * n_days ** 0.5
std return(std)
= (data
aggregated = data.index.year)
.assign(Year "Year >= 2017")
.query("Ticker", "Year"])
.groupby([
.agg(= ("Close", growth),
growth = ("Close", volatility),
volatility = ("Close", "count"),
trading_days = ("Close", "first"),
first = ("Close", "last"),
last = ("Close", "max"),
high = ("Close", "min")
low
) ).reset_index()
I use the reactable R package to build an interactive results table as an htmlwidget. Thanks to reticulate, the handover from Python to R is seamless. The Python dataframe is available as py$aggregated
. One small drawback: the R representation doesn’t include the multi index created by the group by operation in Python, which is why I used reset_index
.
# Define function for conditional styling of cells
<- function(value) {
colors if (value > 0) {
<- "green"
color else if (value < 0) {
} <- "red"
color else {
} <- "#777"
color
}list(color = color, fontWeight = "bold")
}
reactable(
data = py$aggregated,
compact = TRUE,
highlight = TRUE,
showSortable = TRUE,
defaultSorted = "Ticker",
columns = list(
growth = colDef(
name = "Growth",
style = colors,
format = colFormat(percent = TRUE, digits = 2)
),volatility = colDef(name = "Volatility", format = colFormat(digits = 2)),
trading_days = colDef(name = "Trading Days"),
last = colDef(name = "Last", format = colFormat(digits = 2)),
first = colDef(name = "First", format = colFormat(digits = 2)),
high = colDef(name = "High", format = colFormat(digits = 2)),
low = colDef(name = "Low", format = colFormat(digits = 2))
),columnGroups = list(
colGroup(
name = "Stock Price in USD",
columns = c("first", "last", "high" ,"low")
)
) )
Sorting by year reveals that 2018 was a rather bad year for FANGMANT. Apple, Facebook, Nvidia and Google lost in value. But it wasn’t universal: Amazon, Microsoft, Tesla and Netflix rose. The MSCI World took a 9.25% dive.
Stock performance over time
To visualize the stock developments over time, they have to be scaled to the same initial level. Otherwise, all we’d see is the difference in the price of each individual stock.
First, I export the data to R.
= (data
data_chart filter(items = ["Ticker", "Date", "Close"])
.
.reset_index() )
The grouped mutate operation is much easier to do in dplyr than in pandas.
For visualization, I use echarts4r, which I wrote about in a previous article.
$data_chart |>
pygroup_by(Ticker) |>
::mutate(Close = Close / Close[1]) |>
dplyre_charts(x = Date) |>
e_line(serie = Close, symbol = "none") |>
e_tooltip(trigger = "axis") |>
e_axis_labels(y = "Value (indexed)")
Click on the Ticker names to hide individual series. This rescales the axes and allows more detailed views of all time series.
Tesla had the strongest performance, thanks to the amazing 720% growth in 2020. The second winner is Nvidia, which recently experience a strong rise. The MSCI World grew at a comparatively stop but steady pace, yet still reached 204% of its initial valuation.
Growth vs volatility
Stronger growth opportunities typically come at the cost of increased risk. To check how true this is among FANGMANT and the MSCI World as a reference, I plot the yearly returns and volatilities in a scatterplot.
$aggregated |>
py::mutate(type = ifelse(Ticker == "URTH", "MSCI World ETF", "Individual FANGMANT stock")) |>
dplyrgroup_by(type) |>
e_charts(x = growth) |>
e_scatter(
serie = volatility,
symbol_size = 10
|>
) e_axis_labels(x = "Return", y = "Volatility")
In line with theory, the individual stocks have higher volatility than the ETF. There’s a tradeoff between returns and stability.
According to the classic Markowitz model, I’d expect that an analysis that includes more stocks (not just the most famous tech stocks) would show that the average return of stocks is the same as that of the MSCI World, but at a higher volatility. Therefore, it would be better to hold the MSCI World than picking random individual stocks as it is at the efficient frontier.
Conclusion
FANGMANT performed amazingly well in the last 5 years and outperformed the MSCI world. While the MSCI World doubled in 5 years, Facebook, the worst of the FANGMANT performers, tripled in value.
Contrary to other industries, FANGMANT and the tech stocks as a whole were not affected by the pandemic. This also stabilized the MSCI World, which had a dip but recovered within months.
Will FANGMANT continue to outperform the MSCI World? Hundreds of thousands of analysts are trying to figure it out. According to the efficient market hypothesis, all information is already priced in, including expected future developments (new inventions, products, management practices, consumption cycles). An investor without inside information can’t predict the future price. But the theory isn’t without criticism.
Photo by Maxim Hopman on Unsplash