Market indices are vital in finance, offering 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.

In this newsletter, we’ll make a portfolio that replicates the S&P500.

## How to replicate your favorite investment portfolio

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 returns while minimizing its expected loss. We’ll do this while keep a low tracking error relative to the S&P 500.

This constraint is the key for achieving index replication.

Let’s see how it works.

### Imports and set up

We’ll use Riskfolio-Lib to create our portfolio and yFinance for historical data.

import riskfolio as rp import pandas as pd import yfinance as yf import warnings warnings.filterwarnings("ignore")

Next, we’ll download price data for the portfolio we want to weight. The weights will be those that minimize the tracking error between the portfolio and the index.

assets = [ "JCI", "TGT", "CMCSA", "CPB", "MO", "APA", "MMC", "JPM", "ZION", "PSA", "BAX", "BMY", "LUV", "PCAR", "TXT", "TMO", "DE", "MSFT", "HPQ", "SEE", "VZ", "CNP", "NI", "T", "BA", "^GSPC", ] data = yf.download(assets, start="2016-01-01", end="2019-12-30") data = data.loc[:, ("Adj Close", slice(None))] data.columns = assets

The last step is to compute the portfolio and index returns.

returns = data.pct_change().dropna() bench_returns = returns.pop("^GSPC").to_frame()

### Build the portfolio

We’ll build the Riskfolio-Lib portfolio to estimate expected returns and the covariance matrix based on historical data. Then, we’ll set the portfolio attributes to get ready to optimize.

port = rp.Portfolio(returns=returns) port.assets_stats(method_mu="hist", method_cov="hist", d=0.94) port.kindbench = False port.benchindex = bench_returns port.allowTE = True port.TE = 0.008

This code creates the portfolio object using historical returns data. It estimates the expected asset returns and covariance matrix based on historical data. Then it configures the portfolio to use an index with specified returns.

It also sets the tracking error constraint to a maximum limit relative to the benchmark returns.

Tracking error measures the deviation of a portfolio’s returns from a benchmark index. By optimizing our portfolio with a maximum tracking error constraint, we can weight the portfolio so the returns closely match the index returns.

### Run the optimization

Now that our portfolio is set up, we can run the optimization.

model = "Classic" rm = "CVaR" obj = "Sharpe" hist = True rf = 0 l = 0 w = port.optimization( model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist )

This code calculates the optimal portfolio such that the weights maximize the risk adjusted return and minimize the conditional value at risk (otherwise known as CVaR or expected loss).

Riskfolio-Lib does this with the tracking error constraint. This means that while the portfolio is optimized, the tracking error does not exceed 0.008.

We can plot the optimal weights.

ax = rp.plot_pie( w=w, title="Sharpe Mean CVaR", others=0.05, nrow=25, cmap="tab20", height=6, width=10, ax=None, )

The result is a donut plot that shows the weights of the top portfolio assets.

We can also create an efficient frontier. The Efficient Frontier represents a set of optimal portfolios that offer the highest expected return for a given level of risk or the lowest risk for a given level of expected return.

frontier = port.efficient_frontier( model=model, rm=rm, points=50, rf=rf, hist=hist ) ax = rp.plot_frontier( w_frontier=frontier, mu=port.mu, cov=port.cov, returns=port.returns, rm=rm, rf=rf, cmap="viridis", w=w, label="Max Risk Adjusted Return Portfolio", marker="*", )

The result is our Efficient Frontier.

### Next steps

With Riskfolio-Lib, you can use several different portfolio models including Black-Litterman and factor models to estimate returns. You can also use many different risk measures to optimize. As a next step, try building your own portfolio and experiment with the different expected return and risk models.