Beat the Market? Optimizing Your Top IHSG Stock Portfolio in Python

Forget Complex Math: Just use this basic stats trick and optimize your portfolio like a hedge fund pro.

Mohamad Nur Syahril Kaharu
Data Science Indonesia
7 min readMar 14, 2024

--

Over the last five years, the top 50 hedge funds in America collectively returned annualized gains that were more than twice the industry’s average (15.5 percent).¹ While many believe these impressive gains stem from complex algorithms and calculations, the truth is far simpler: these funds leverage basic statistical concepts like mean and variance to guide their investment decisions. They use a metric called the Sharpe Ratio which consists of mean and variance calculations of the portfolio.

The Sharpe Ratio is a widely used and relatively simple metric in portfolio analysis. It helps assess an investment’s or portfolio’s risk-adjusted performance. In simpler terms, it tells you how much extra return you’re getting for taking on additional risk.

The Sharpe Ratio is calculated by dividing the excess return by the volatility. The bigger the ratio, the bigger the returns per risk of the portfolio, so it would make the portfolio more profitable.

Sharpe Ratio = (Rp — Rf / σp)

Where:

  • Rp : Expected return of the portfolio
  • Rf : Risk-free rate of return (e.g., government bond yield)
  • σp : Standard deviation of the portfolio’s returns (measure of volatility or risk)

In this article, we are going to discuss how to maximize stock portfolio returns using the sharpe ratio concept. We use the case for certain popular stocks in IHSG (Indonesian Stock Index) which are UNVR (Unilever Indonesia), BRIS (Bank Syariah Indonesia), ASII (Astra Internasional Indonesia), TLKM (Telkom Indonesia), and BSSR (Baramulti Suksessarana Indonesia). Given that we are going to invest in those stocks, how many proportions of each stock should we invest to get maximum returns. This question will be answered later in this article.

Setting up the Tools Needed

All the code is provided in my github portfolio optimization project. We use Python to perform the optimization so kindly install Python in your environment.

Several libraries are needed in this project, including yfinance for extracting the dataset and pypfopt for performing portfolio optimization. We can just install all the requirement libraries in the requirements_portopt.txt provided in the github using the command :

pip install -r requirements_portopt.txt

We suggest the reader use a dedicated virtual environment when working on this project to avoid any mismatching library version during installation. So before installing all the requirements, make the virtual environment using the following commands :

## make the dedicated venv
python -m venv your_project_name

## to enter the venv
source your_project_name/bin/activate

Cracking the Data

First of all, import all the required libraries.

import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
plt.style.use('fivethirtyeight')

We are going to get the stock values for these stocks: UNVR, BRIS, ASII, TLKM, and BSSR. Store all of the stocks in a list. Remember that you can add the JK suffix to specify the indonesian stocks.

# List of stocks
assets = ['UNVR.JK', 'BRIS.JK', 'ASII.JK', 'TLKM.JK', 'BSSR.JK']

We are going to assess the stock values in the entire year of 2023.

start_date = '2023-01-01'
end_date = '2024-01-01'

Finally get the stock prices using yfinance library and store it into a pandas dataframe.

df = pd.DataFrame()

for stock in assets:
df[stock] = yf.download(stock, start=start_date, end=end_date)['Adj Close']

You should see this output for the dataframe.

Let’s visualize the stock prices for a second.

# Visually show the stocks
title = 'Alil Portofolio Adj. Close Price History'

# Get the stock
my_stocks = df

# Create and plot the graph
for c in my_stocks.columns.values:
plt.plot(my_stocks[c], label=c)

plt.title(title)
plt.xlabel('Date', fontsize=18)
plt.ylabel('Adj. Price IDR (RP)', fontsize=18)
plt.legend(my_stocks.columns.values, loc='upper left')
plt.show()

We can see that all of the stock prices have positive returns during the year except for the UNVR stock which got a negative return. This later will impact the optimization calculation.

Let’s see the daily returns of each stock.

returns = df.pct_change()
returns

Make it annual returns using covariance matrix.

cov_matrix_annual = returns.cov() * 252 # 252 is the number of trading days in a year
cov_matrix_annual

In the covariance matrix above, the diagonal elements represent the variance of each stock. Variance tells you how much the individual stock’s returns fluctuate around its average return.The values off the diagonal represent the covariance between two different stocks. Covariance indicates how the returns of two stocks tend to move together. Here’s a breakdown of what the covariance values signify:

  • 0: No link between returns.
  • -1: Opposite direction (one goes up, the other down).
  • +1: Same direction (both up or down).

Evaluate the Portfolio for Proportional Split

Let’s evaluate if we invest in those stocks proportionally with 20% each, how much return and risk we would get.

Firstly store the weights in the list. Each stock gets 20% proportion.

# set weights
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

Calculate the variance of the portfolio with the proportional allocation.

# calculate portofolio variance
port_variance = np.dot(weights, np.dot(cov_matrix_annual, weights))
port_variance # = 0.01941691045863958 or around 2%

Calculate the risk or volatility of the portfolio which is basically the standard deviation or the square root of variance.

# calculate the portfolio volatility/standard deviation
port_volatility = np.sqrt(port_variance)
port_volatility # = 0.1393445745576037 or around 14%

Finally, calculate the simple annual return.

# calculate the annual portfolio return
portofolioSimpleAnnualReturn = np.sum(returns.mean() * weights) * 252
portofolioSimpleAnnualReturn # = 0.1092809092124986 or around 11%

Assuming that the annual return for a risk-free portfolio like government bonds is 2.5% we can calculate the sharpe ratio of the portfolio:

Sharpe Ratio = (Rp — Rf / σp) = (11% — 2.5%)/14% = 0.6

The 0.6 sharpe ratio is considered low. So how to optimize it then? We’ll see in the next section.

Maximize The Portfolio Return

First of all, let’s import the required libraries.

from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import risk_models
from pypfopt import expected_returns

We are going to get the best proportion of each stock that resulted the maximum sharpe ratio.

# Get the expected returns and covariance matrix
mu = expected_returns.mean_historical_return(df)
S = risk_models.sample_cov(df)

# optimize for max sharpe ratio
ef = EfficientFrontier(mu, S)
weights = ef.max_sharpe() # maximizing sharpe ratio, how much the return increase in every risk increase
cleaned_weights = ef.clean_weights() # rounding some near zero weights
print(cleaned_weights)

# print portofolio allocation
ef.portfolio_performance(verbose=True)

We get this output

OrderedDict([('UNVR.JK', 0.0), ('BRIS.JK', 0.49753), ('ASII.JK', 0.23315), ('TLKM.JK', 0.1236), ('BSSR.JK', 0.14572)])
Expected annual return: 24.0%
Annual volatility: 21.4%
Sharpe Ratio: 1.03

The Sharpe ratio we calculated (1.0) is significantly better than the previous value (0.6). While it still falls within the “adequate” range, it indicates a stronger return relative to the risk involved.

To achieve this improved Sharpe ratio, we recommend investing in the following proportions:

BRIS: 50%
ASII: 23%
TLKM: 12%
BSSR: 15%
UNVR is excluded due to its previously calculated negative returns, which would negatively impact the portfolio’s overall performance.

Now, let’s consider how to allocate a specific investment amount, say IDR 10 million, to maximize returns based on this Sharpe ratio.

# Get the discrete allocations for each share with amount of money available
from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices

latest_prices = get_latest_prices(df) # get latest price of each stocks
weights = cleaned_weights
da = DiscreteAllocation(weights, latest_prices, total_portfolio_value= 10000000)

allocation, leftover = da.lp_portfolio()
print('Discrete allocation:', allocation)
print('Funds remaining: Rp {:.2f}'.format(leftover))
Discrete allocation: {'BRIS.JK': 2861, 'ASII.JK': 412, 'TLKM.JK': 313, 'BSSR.JK': 389}
Funds remaining: Rp 1988.13

Based on the calculated allocation and with a starting capital of IDR 10 million, here’s the recommended breakdown of our portfolio:

  • BRIS: 2,861 shares
  • ASII: 412 shares
  • TLKM: 313 shares
  • BSSR: 389 shares

This allocation maximizes your portfolio’s return potential based on the Sharpe ratio. It’s important to note that due to potential fractional share limitations by some brokerages, you might end up with a slightly different number of shares and a small remaining cash balance which is IDR 1988.13.

Conclusion

Simpler than you think! The secret sauce behind the Sharpe Ratio, which helps get the most out of your portfolio, is basically just about averages (mean) and how much things bounce around (variance). These are like the ABCs of statistics, something even a middle schooler can grasp.

So, once you’ve picked your stocks using your awesome analysis skills, this Sharpe Ratio thing can help you divvy up your cash between them. The goal? Maximize returns while keeping risk low. Basically, it’s like having a superpower to build a killer portfolio — watch out hedge fund managers!

References

  1. https://www.riaintel.com/article/2aucrzsa72lr93xz8ghds/investments/the-most-consistently-profitable-hedge-funds-continue-to-prove-their-edge
  2. https://github.com/alilsyahril/Portofolio_Optimization

--

--