MAANG Stocks: Analysis¶
This notebook downloads one year of daily adjusted close prices for MAANG stocks (Meta, Apple, Amazon, Netflix, Google). It
- computes daily simple returns and log returns,
- summarizes their distributional properties,
- compares empirical distributions to a normal approximation using sample mean and standard deviation,
- evaluates serial dependence using autocorrelation and Ljung-Box tests.
import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yfinance as yf
from scipy.stats import jarque_bera, kurtosis, norm, skew
from statsmodels.stats.diagnostic import acorr_ljungbox
from statsmodels.tsa.stattools import acf
# Define the MAANG tickers for analysis
tickers = ['META', 'AAPL', 'AMZN', 'NFLX', 'GOOGL']
# Download one year of daily adjusted close prices
end_date = datetime.date.today()
start_date = end_date - datetime.timedelta(days=365)
prices = yf.download(tickers, start=start_date, end=end_date, progress=False, actions=False)["Close"]
prices = prices.dropna(how='all')
# Compute daily simple returns and log returns
returns = prices.pct_change().dropna()
log_returns = np.log(prices / prices.shift(1)).dropna()
Distributional Properties¶
data.std(ddof=1): unbiased sample standard deviation with Bessel's correction (degrees of freedom = $n-1$): $$ s = \sqrt{\frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2}\,. $$ See https://pandas.pydata.org/docs/reference/api/pandas.Series.std.htmlskew(data, bias=False): unbiased sample skewness. The adjusted estimator is: $$ G_1 = \frac{\sqrt{n(n-1)}}{n-2} \frac{m_3}{m_2^{3/2}}\,, $$ where $m_k = \frac{1}{n}\sum_{i=1}^n (x_i - \bar{x})^k$ is the k-th central moment and $n$ is the sample size. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.skew.htmlkurtosis(data, fisher=True, bias=False): unbiased excess kurtosis. The adjusted estimator is: $$ G_2 = \frac{(n-1)}{(n-2)(n-3)}\left((n+1)g_2 + 6\right)\,, $$ where $g_2 = \frac{m_4}{m_2^2} - 3$ is the biased excess kurtosis estimator, and $m_k$ is the k-th central moment. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kurtosis.htmljarque_bera(data): Jarque-Bera test statistic for normality: $$ \mathrm{JB} = \frac{n}{6} \left(S^2 + \frac{(K-3)^2}{4} \right)\,, $$ where (S) is skewness and (K) is kurtosis. Returns(JB, p-value). See https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.jarque_bera.html
pd.set_option("display.multi_sparse", True)
summary = []
for label, series in [('Returns', returns), ('Log Returns', log_returns)]:
for ticker in tickers:
data = series[ticker]
stat = {
'Ticker': ticker,
'Type': label,
'Mean': data.mean(),
'Std Dev': data.std(ddof=1),
'Skewness': skew(data, bias=False),
'Kurtosis': kurtosis(data, fisher=True, bias=False),
}
jb_stat, jb_pvalue = jarque_bera(data)
stat['Jarque-Bera'] = jb_stat
stat['JB p-value'] = jb_pvalue
summary.append(stat)
summary_df = pd.DataFrame(summary)
summary_df = summary_df.sort_values(by=['Ticker', 'Type'])
summary_df = summary_df.round(6)
summary_df
| Ticker | Type | Mean | Std Dev | Skewness | Kurtosis | Jarque-Bera | JB p-value | |
|---|---|---|---|---|---|---|---|---|
| 6 | AAPL | Log Returns | 0.001540 | 0.014244 | 0.193638 | 1.606197 | 26.467302 | 0.000002 |
| 1 | AAPL | Returns | 0.001643 | 0.014288 | 0.268632 | 1.600978 | 27.724448 | 0.000001 |
| 7 | AMZN | Log Returns | 0.000545 | 0.018975 | -0.055314 | 3.473778 | 118.682638 | 0.000000 |
| 2 | AMZN | Returns | 0.000725 | 0.018987 | 0.099364 | 3.583976 | 126.658372 | 0.000000 |
| 9 | GOOGL | Log Returns | 0.002902 | 0.018267 | 1.063048 | 3.909024 | 196.698681 | 0.000000 |
| 4 | GOOGL | Returns | 0.003074 | 0.018507 | 1.195354 | 4.509515 | 259.031316 | 0.000000 |
| 5 | META | Log Returns | -0.000677 | 0.022468 | -0.090636 | 7.438957 | 548.145389 | 0.000000 |
| 0 | META | Returns | -0.000426 | 0.022462 | 0.220936 | 7.388606 | 542.394973 | 0.000000 |
| 8 | NFLX | Log Returns | -0.001624 | 0.020942 | -0.081695 | 9.554998 | 905.382883 | 0.000000 |
| 3 | NFLX | Returns | -0.001405 | 0.020923 | 0.286153 | 10.509491 | 1098.840831 | 0.000000 |
The table above summarizes the first- and second-moment statistics, skewness, excess kurtosis, and Jarque-Bera normality test results for both simple and log returns. The Jarque-Bera test is especially useful for detecting departures from the normal distribution due to skewness and kurtosis.
Empirical Distribution with Normal Overlay¶
Plot the empirical distribution of log returns for each ticker and overlay a normal density using the sample mean and standard deviation. This helps visualize departures from normality.
fig, axes = plt.subplots(len(tickers)+1, 1, figsize=(4, 4*len(tickers)))
axes = axes.flatten()
for idx, ticker in enumerate(tickers):
data = log_returns[ticker].dropna()
mu = data.mean()
sigma = data.std(ddof=1)
x = np.linspace(data.min(), data.max(), 200)
ax = axes[idx]
ax.hist(data, bins=30, density=True, alpha=0.6, color=f'C{idx}', edgecolor='black')
ax.plot(x, norm.pdf(x, loc=mu, scale=sigma), color='black', linewidth=2, label='Normal approx')
ax.axvline(mu, color='gray', linestyle='--', linewidth=1)
ax.set_title(f'{ticker} Empirical Distribution')
ax.set_xlabel('Log Return')
ax.set_ylabel('Density')
ax.legend()
axes[-1].set_visible(False)
plt.tight_layout()
plt.show()
Autocorrelation Analysis¶
Compute the autocorrelation function (ACF) of log returns and then assess serial dependence with the Ljung-Box test.
nlags = 5
acf_results = {}
for ticker in tickers:
acf_results[ticker] = acf(log_returns[ticker], nlags=nlags, fft=False)
acf_df = pd.DataFrame(acf_results, index=[f'Lag {i}' for i in range(nlags + 1)])
acf_df
| META | AAPL | AMZN | NFLX | GOOGL | |
|---|---|---|---|---|---|
| Lag 0 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
| Lag 1 | 0.016932 | 0.095279 | 0.089386 | 0.059752 | 0.065097 |
| Lag 2 | -0.019335 | -0.008767 | -0.010252 | 0.073500 | -0.016948 |
| Lag 3 | -0.075196 | 0.052104 | 0.027675 | 0.024893 | 0.064377 |
| Lag 4 | -0.005060 | -0.030008 | -0.019361 | -0.006440 | 0.046376 |
| Lag 5 | 0.144932 | -0.017792 | 0.091321 | -0.021232 | 0.047239 |
fig, ax = plt.subplots(figsize=(12, 6))
for ticker in tickers:
ax.plot(range(nlags + 1), acf_results[ticker], marker='o', label=ticker, linewidth=2)
ax.axhline(0, color='black', linestyle='--', linewidth=0.8)
ax.set_xlabel('Lag')
ax.set_ylabel('Autocorrelation')
ax.set_title('ACF of Log Returns for MAANG Stocks')
ax.set_xticks(range(nlags + 1))
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Ljung-Box Test for Serial Correlation¶
The Ljung-Box test checks whether there is statistically significant autocorrelation across multiple lags simultaneously. A p-value below 0.05 suggests evidence against the null hypothesis of no autocorrelation.
results = []
for ticker in tickers:
lb_test = acorr_ljungbox(log_returns[ticker], lags=nlags, return_df=True)
lb_test['Ticker'] = ticker
results.append(lb_test)
lb_df = pd.concat(results, ignore_index=False)
lb_df = lb_df.reset_index().rename(columns={'index': 'Lag'})
lb_df['Lag'] = lb_df['Lag'] + 1
lb_df = lb_df[['Ticker', 'Lag', 'lb_stat', 'lb_pvalue']].rename(columns={
'lb_stat': 'LB Statistic',
'lb_pvalue': 'p-value'
})
lb_df
| Ticker | Lag | LB Statistic | p-value | |
|---|---|---|---|---|
| 0 | META | 2 | 0.072253 | 0.788084 |
| 1 | META | 3 | 0.166844 | 0.919963 |
| 2 | META | 4 | 1.603413 | 0.658616 |
| 3 | META | 5 | 1.609945 | 0.807004 |
| 4 | META | 6 | 6.990275 | 0.221365 |
| 5 | AAPL | 2 | 2.287778 | 0.130396 |
| 6 | AAPL | 3 | 2.307226 | 0.315495 |
| 7 | AAPL | 4 | 2.996958 | 0.392095 |
| 8 | AAPL | 5 | 3.226664 | 0.520635 |
| 9 | AAPL | 6 | 3.307751 | 0.652655 |
| 10 | AMZN | 2 | 2.013546 | 0.155901 |
| 11 | AMZN | 3 | 2.040141 | 0.360570 |
| 12 | AMZN | 4 | 2.234723 | 0.525142 |
| 13 | AMZN | 5 | 2.330350 | 0.675249 |
| 14 | AMZN | 6 | 4.466465 | 0.484383 |
| 15 | NFLX | 2 | 0.899754 | 0.342848 |
| 16 | NFLX | 3 | 2.266710 | 0.321951 |
| 17 | NFLX | 4 | 2.424137 | 0.489157 |
| 18 | NFLX | 5 | 2.434716 | 0.656363 |
| 19 | NFLX | 6 | 2.550181 | 0.768920 |
| 20 | GOOGL | 2 | 1.067935 | 0.301412 |
| 21 | GOOGL | 3 | 1.140616 | 0.565351 |
| 22 | GOOGL | 4 | 2.193547 | 0.533221 |
| 23 | GOOGL | 5 | 2.742206 | 0.601850 |
| 24 | GOOGL | 6 | 3.313803 | 0.651729 |
Ljung-Box Interpretation¶
If the Ljung-Box test fails to reject the null hypothesis, the log returns are consistent with a white noise process. A white noise series is defined as a sequence of random variables with constant mean, constant variance, and zero autocorrelation at all non-zero lags.
In this notebook, the Ljung-Box results do not provide strong evidence of predictive lagged structure in the MAANG log returns, supporting the idea that the series behaves like white noise over this sample period.