Easily build your own technology index with the Mag 7
Easily build your own technology index with the Mag 7
Market indices are vital in finance.
They give investors a quick look at the overall performance of a specific market or sector.
They act as benchmarks for assessing the performance of different investment portfolios.
Investors can copy the performance of an index for exposure to securities without the need to buy each one.
This process is called index replication.
Sometimes, our replicated portfolio can even outperform the benchmark, which we’ll see today.
In today’s newsletter, you’ll build a portfolio of Magnificent 7 mega-cap tech stocks that improves on the Nasdaq ETF, QQQ.
Let's go!
Easily build your own technology index with the Mag 7
Index replication is a strategy that lets investors mimic the performance of market indices. All without holding each asset in the index. These indices comprise various securities that reflect a particular market segment.
For investors following market indices, many make investments in the individual assets to replicate it.
However, this method can prove expensive, particularly for individual investors. So to replicate an index, we can weight a smaller portfolio to mimic index returns. That's where Riskfolio-Lib comes in.
We’ll use Riskfolio-Lib to optimize a portfolio's Sharpe ratio while minimizing its expected shortfall measured by Conditional Value-at-Risk (CVaR). We'll do this while keep a low tracking error relative to the Nasdaq.
This constraint is the key for achieving index replication.
Let's see how it works.
Fetch historical stock data and calculate returns
We will fetch historical stock price data for a list of assets from Yahoo Finance. This data will be used to calculate daily returns.
1import riskfolio as rp
2import pandas as pd
3import yfinance as yf
4
5
6assets = ["QQQ", "AAPL", "MSFT", "AMZN", "GOOGL", "META", "TSLA", "NVDA"]
7
8data = yf.download(assets, start="2016-01-01", end="2019-12-30")["Adj Close"]
9data.index = pd.to_datetime(data.index)
10
11returns = data.pct_change().dropna()
12bench_returns = returns.pop("QQQ").to_frame()
13
Using the yfinance
library, we download adjusted closing price data. We calculate daily percentage returns for the assets and store them in a DataFrame, dropping any missing values. The benchmark returns for the QQQ ETF, are separated for later comparison.
Set up and configure the portfolio
Next, we will set up a portfolio object to store the asset returns and configure it with desired statistical methods. We also define some constraints for the optimization process.
1port = rp.Portfolio(returns=returns)
2port.assets_stats(method_mu="hist", method_cov="hist", d=0.94)
3
4port.kindbench = False
5port.benchindex = bench_returns
6
7port.allowTE = True
8port.TE = 0.008
We create a portfolio object using the riskfolio
library and load the calculated returns into it.
We then calculate asset statistics like mean and covariance using historical data, setting the decay factor for exponential smoothing.
We indicate that there are no predefined benchmark weights, choosing instead to use the benchmark index we previously defined. We enable the use of tracking error constraints, which will allow us to control how much the portfolio's returns can deviate from the benchmark.
We set a maximum tracking error of 0.8%, ensuring our portfolio remains closely aligned with the benchmark.
Optimize the portfolio and visualize the result
We will perform the portfolio optimization using the specified risk model and objective function. After obtaining the optimal weights, we will visualize the portfolio composition with a pie chart.
w = port.optimization(
model="Classic",
rm="CVaR",
obj="Sharpe",
rf=0,
l=0,
hist=True
)
ax = rp.plot_pie(
w=w,
title="Sharpe Mean CVaR",
others=0.05,
nrow=25,
cmap="tab20",
height=6,
width=10,
ax=None,
)
We perform the portfolio optimization using a classic mean-variance model. The risk measure chosen is CVaR, which focuses on tail risk. We aim to maximize the Sharpe ratio, which balances risk and return.
The result is a set of optimal asset weights, which we then visualize using a pie chart.
The pie chart helps us understand the composition of the portfolio, showing the relative weight of each asset. We also group smaller allocations under "others" for simplicity.
Compare portfolio and benchmark returns
We will calculate the cumulative returns for both the optimized portfolio and the benchmark. Finally, we will plot these returns to visually compare their performance over time.
1portfolio_returns = returns.dot(w)
2
3comparison = pd.concat([portfolio_returns, bench_returns], axis=1)
4
5comparison.add(1).cumprod().plot(figsize=(10, 6), title="Cumulative Returns: QQQ vs Portfolio")
The result is a performance comparison between the benchmark and the portfolio.
We calculate the portfolio's returns by multiplying the asset returns by their respective weights. This gives us a time series of portfolio returns.
We then create a DataFrame that combines these portfolio returns with the benchmark returns, aligning them for comparison. We calculate cumulative returns by adding one to each return and taking the cumulative product over time.
Finally, we plot these cumulative returns to visually compare the performance of the optimized portfolio against the QQQ benchmark. The resulting plot provides a clear view of how well the portfolio tracks the benchmark and highlights any periods of outperformance or underperformance.
Your next steps
The portfolio vastly outperforms the benchmark. What’s the explanation?
The optimizer weights the stocks which maximize Sharpe ratio and minimize CVaR given the constraint to also minimize the tracking error. Because the stocks that did that vastly outperformed the market, we actually create a superior portfolio.