Quick Introduction

This tutorial is a supplementary resource for the workshop “Stock Portfolio Optimization with Python” using the Sectors API. It covers these topics: stock investment background and understanding the Sectors API, stock selection overview and two stock portfolio optimization models, followed by further recommendations and insights.

Sectors API & Stock Investment Overview

When considering stock investments, individual investors typically focus on key questions such as: Which stocks to buy? How many shares? What’s the price, growth potential, and risk?

Using the Sectors API, investors gain access to valuable data from the “Companies by Index” and “Daily Transaction Data” endpoints, including stock indexes, dates, closing prices, volumes, and market capitalizations. Next, the “Company Report” API provides additional insights such as EPS, dividends, growth metrics, and other relevant financial information.

In Data Collection from Sectors API, we will explore how to effectively query this data from the API to build a portfolio optimization model. However, before diving into optimization, it’s crucial to understand the available stock indexes and their respective investment purposes.

indexIndexFocusInvestment Use
0FTSEGlobally recognized index of large-cap companiesFor investors seeking to track global or broad market performance
1IDX30Top 30 stocks by market cap and liquiditySuitable for blue-chip stock investors looking for stable and liquid companies
2IDXBUMN20Top 20 government-owned enterprises (BUMN)For exposure to state-owned enterprises (SOEs) benefiting from government policies
3IDXESGLStocks meeting ESG (environmental, social, governance) standardsIdeal for socially responsible investors focused on sustainability and ethical investing
4IDXG30Large-cap, high liquidity growth stocksFor growth-oriented investors looking for long-term capital appreciation
5IDXHIDIV2020 stocks with high dividend yieldsAttractive to income-seeking investors focused on dividend income
6IDXQ30Focus on quality stocks based on financial metricsSuitable for long-term investors seeking companies with strong fundamentals
7IDXV30Focus on value stocks trading below intrinsic valueIdeal for value investors looking for undervalued stocks
8JII7070 stocks complying with Shariah (Islamic law)Suitable for Shariah-compliant investors following Islamic investment principles
9KOMPAS100100 most liquid, actively traded stocksFor investors seeking diversified exposure to Indonesia’s liquid stocks
10LQ45Top 45 most liquid stocks with large market capsBlue-chip focused, for investors looking for stability and long-term growth potential
11SMInfra1818 infrastructure-related stocksFor investors bullish on infrastructure growth and development projects in Indonesia
12SRIKEHA18Tracks sustainability and social responsibilityIdeal for ESG investors prioritizing sustainable business practices
13SRIKEHATISustainability and ethical investingSame as SRIKEHA18, for socially responsible investors

Which index caught your attention? What do you want to choose?

Where to start?

Recommendation: Match your investment goals with the property of stock index.

indexCriteriaDescriptionPersonaRecommended Index
0Liquidity & StabilityInvestors looking for highly liquid and stable stocks that are less volatile.Maria, a risk-averse retiree seeking low volatility and stable returns.IDX30, LQ45, KOMPAS100
1Government-Owned EnterprisesFor investors interested in companies benefiting from government backing and policies.Adi, a public sector enthusiast who trusts government-driven initiatives.IDXBUMN20
2Dividend FocusIdeal for those seeking regular income from dividends.Siti, a conservative investor who prefers stable income from dividends.IDXHIDIV20
3Growth-OrientedSuitable for long-term investors focusing on capital appreciation.Kevin, a young professional aiming for long-term wealth through capital growth.IDXG30
4Value StocksInvestors seeking undervalued stocks trading below intrinsic value.Tom, a value investor who looks for bargain stocks below intrinsic value.IDXV30
5High-Quality FinancialsFocus on stocks with strong fundamentals and good financial health.Dewi, a financial analyst who invests in companies with strong fundamentals.IDXQ30
6Shariah-Compliant InvestmentsFor investors following Islamic principles.Ahmad, a devout Muslim who prioritizes Shariah-compliant investments.JII70
7Socially Responsible InvestmentsInvestors interested in ESG (environmental, social, governance) and ethical business practices.Sarah, an environmentally conscious investor focusing on ethical companies.IDXESGL, SRIKEHA18, SRIKEHATI
8Infrastructure FocusFor investors bullish on Indonesia’s infrastructure growth.Indra, an infrastructure expert optimistic about Indonesia’s construction growth.SMINFA18
9Broad Market ExposureFor investors wanting diversified exposure across large segments of the market.Emily, a diversified investor looking for broad market exposure and lower risk.FTSE, KOMPAS100

While it’s crucial to evaluate a stock’s internal characteristics—such as its nature, properties, and the type of investor it appeals to—external macroeconomic factors play an equally important role in guiding stock index selection. These broader economic conditions, including inflation rates, government policies, and global market trends, are key determinants in the performance of various sectors. Therefore, understanding both your investment persona and the prevailing macroeconomic landscape is essential for making informed decisions.

Now let’s assume my client is Indra, an infrastructure expert optimistic about Indonesia’s construction growth. To confirm focusing solely on SMINFA18, we need to observe the following macroeconomic factors:

  • Government infrastructure spending: Announcements of new airports and highways as public projects.
  • Interest Rate policies: Indonesia’s central bank lowers interest rates.
  • Commodity prices: Steel and cement prices drop due to global supply chain restructuring.
  • Foreign Direct investment: China increases investment in Indonesia’s high-speed rail projects.

We will build the following content for our client Indra.

Data Collection from Sectors API

1. Stock Price Information

# Retreive Stock index from "Companies by Index" API
import time
import requests
from google.colab import userdata

# Retrieve the API key securely
api_key = userdata.get('SECTORS_API_KEY')

# Define the API URL
url = "https://api.sectors.app/v1/index/sminfra18/"

# Pass the API key in the header
headers = {"Authorization": api_key}

# Make the API request
response_company_index = requests.get(url, headers=headers)

print(response_company_index.text)
[
  {
    "symbol": "ADHI.JK",
    "company_name": "PT Adhi Karya (Persero) Tbk."
  },
  {
    "symbol": "AKRA.JK",
    "company_name": "PT AKR Corporindo Tbk."
  },
  {
    "symbol": "BBNI.JK",
    "company_name": "PT Bank Negara Indonesia (Persero) Tbk"
  },
  ...
]
# Retrieve date and price from "Daily Transaction Data" API
from datetime import datetime, timedelta

# Function to calculate the date 90 days ago from today
def calculate_start_date(days_ago=90):
    return (datetime.now() - timedelta(days=days_ago)).strftime('%Y-%m-%d')

# Calculate the start date 90 days ago
start_date = calculate_start_date()

# Looping API info
history_sminfra18 = []

2. Company Report Information

for i, x in enumerate(response_company_index.json()):

  # Define the URL for the API endpoint
  url = "https://api.sectors.app/v1/daily/" + response_company_index.json()[i]['symbol'] + "/"

  # Define the query string with the calculated start date
  querystring = {"start": start_date}

  headers = {"Authorization": userdata.get('SECTORS_API_KEY')}

  response_daily_transaction_data = requests.request("GET", url, headers=headers, params=querystring)

  # Append the result into target list
  history_sminfra18.append(response_daily_transaction_data.json())

  time.sleep(1)
history_sminfra18
[[{'symbol': 'ADHI.JK',
 'date': '2024-07-10',
 'close': 250,
 'volume': 80145800,
 'market_cap': 2101902442496},
{'symbol': 'ADHI.JK',
 'date': '2024-07-11',
 'close': 248,
 'volume': 37656100,
 'market_cap': 2085087215616},
{'symbol': 'ADHI.JK',
 'date': '2024-07-12',
 'close': 262,
 'volume': 138657900,
 'market_cap': 2202793672704},
 ...
]]

Which Stock to choose?

1. Data Preprocessing

How to select stocks?

For our client Indra, who’s focused on stocks from the SMInfra18 index and doesn’t want to invest in the ente it’s time to carefully select the right stocks.

Several key economic factors should guide stock selection from an index:

  • Diversification by Industry and Sector: Ensuring exposure across different sectors to reduce risk.
  • Diversification by Correlation and Returns: Selecting stocks that offer varied returns and are not too closely correlated.
  • Company Growth and Dividend Information: Choosing companies with strong growth potential and reliable dividend payouts.

Other factors, like recent projects, mergers, acquisitions, and broader macroeconomic or geopolitical policies, are also worth considering, though we won’t delve into them here.

In this section, we will employ a range of data analysis techniques, along with our expertise in stock selection, to identify two suitable stocks from a pool of 18 for our client, Indra, to invest in.

Step 1: Create a Dataframe for historical stock performance

# Import all useful libraries
import numpy as np
import pandas as pd

# Flatten the list of lists into a single list of dictionaries
flattened_data = [item for sublist in history_sminfra18 for item in sublist]

# Convert to a pandas DataFrame
df_history_sminfra18 = pd.DataFrame(flattened_data)

# Ensure 'date' is in datetime format
df_history_sminfra18['date'] = pd.to_datetime(df_history_sminfra18['date'])

# Enforce in case any column needs conversion (e.g., 'close', 'volume', 'market_cap')
df_history_sminfra18['close'] = pd.to_numeric(df_history_sminfra18['close'], errors='coerce')
df_history_sminfra18['volume'] = pd.to_numeric(df_history_sminfra18['volume'], errors='coerce')
df_history_sminfra18['market_cap'] = pd.to_numeric(df_history_sminfra18['market_cap'], errors='coerce')

# Check for the first few rows to ensure everything is correct
print(df_history_sminfra18)
      symbol       date  close     volume      market_cap
0    ADHI.JK 2024-07-10    250   80145800   2101902442496
1    ADHI.JK 2024-07-11    248   37656100   2085087215616
2    ADHI.JK 2024-07-12    262  138657900   2202793672704
3    ADHI.JK 2024-07-15    256   49577700   2152348123136
4    ADHI.JK 2024-07-16    248   29521600   2085087257962
..       ...        ...    ...        ...             ...
409  UNTR.JK 2024-08-05  24425    8787200  88071391412224
410  UNTR.JK 2024-08-06  24150    7129900  87708215017472
411  UNTR.JK 2024-08-07  24725    4726300  89796500258816
412  UNTR.JK 2024-08-08  24625    3126500  89433323864064
413  UNTR.JK 2024-08-09  24700    2528100  91340020908032

[414 rows x 5 columns]
 We aim to have all the stock price evolution in the past 90 days in one dataframe

#Pivot the DataFrame so that the 'symbol' becomes the column names and 'date' becomes the index
df_pivot_history_sminfra18 = df_history_sminfra18.pivot_table(index='date', columns='symbol', values='close')
df_pivot_history_sminfra18.head()
dateADHI.JKAKRA.JKBBNI.JKBBRI.JKBMRI.JKEXCL.JKINTP.JKISAT.JKJSMR.JKMEDC.JKPGAS.JKPTPP.JKSMGR.JKSSIA.JKTBIG.JKTLKM.JKTOWR.JKUNTR.JK
2024-07-10 00:00:00250.01510.04820.04850.06375.02270.07325.011000.05125.01340.01520.0392.04050.01070.01935.03160.0780.023550.0
2024-07-11 00:00:00248.01505.04870.04840.06400.02270.07400.011500.05300.01330.01560.0388.04010.01110.01990.03180.0785.023500.0
2024-07-12 00:00:00262.01500.05025.04900.06425.02280.07500.011400.05375.01315.01580.0416.04090.01095.02000.03220.0810.023500.0
2024-07-15 00:00:00256.01505.05025.04820.06350.02240.07375.011425.05300.01285.01580.0406.04060.01100.01970.03160.0800.023750.0
2024-07-16 00:00:00248.01505.04980.04730.06350.02240.07350.011650.05325.01295.01610.0398.04060.01070.02010.03100.0790.023800.0

Step 2: Create a Dataframe for company report

df_sminfra18_company_report = pd.DataFrame(prepared_data)
df_sminfra18_company_report
indexsymbolcompany_nameindustrysub_industrysectoreps_estimaterevenue_estimateeps_growthrevenue_growthtotal_dividendsavg_yield_dividends
0ADHI.JKPT Adhi Karya (Persero) Tbk.Heavy Constructions & Civil EngineeringHeavy Constructions & Civil EngineeringInfrastructures0.015655500000000-1.0-0.22007148280407317.06940.00909244380891323
1AKRA.JKPT AKR Corporindo Tbk.Oil & GasOil & Gas Storage & DistributionEnergy158.97454644000000000.1284976449873480.080249278422712125.00.0483752990141511
2BBNI.JKPT Bank Negara Indonesia (Persero) TbkBanksBanksFinancials679.11716178000000000.2100477746188420.162937331535753280.4950.0517167726531625
3BBRI.JKPT Bank Rakyat Indonesia (Persero) TbkBanksBanksFinancials451.372138010000000000.1334205046825360.284275264842541319.00.0377051925286651
4BMRI.JKPT Bank Mandiri (Persero) TbkBanksBanksFinancials657.581617270000000000.1146761569181090.312340707462218353.9580.043529000878334
5EXCL.JKPT XL Axiata TbkWireless Telecommunication ServicesWireless Telecommunication ServicesInfrastructures163.4367261000000000.6803819836286720.13623415356617942.00.011895396001637
6INTP.JKIndocement Tunggal Prakarsa TbkConstruction MaterialsConstruction MaterialsBasic Materials520.1519834300000000-0.08490794986104950.104989950838329160.00.0379668477922678
7ISAT.JKPT Indosat TbkWireless Telecommunication ServicesWireless Telecommunication ServicesInfrastructures804.29586065000000000.4390080134917390.144015096825843255.70.0401271820068359
8JSMR.JKPT Jasa Marga TbkTransport Infrastructure OperatorHighways & RailtracksInfrastructures412.8318012300000000-0.558954239095396-0.15509011963962975.69390.00636524613946676
9MEDC.JKPT Medco Energi Internasional TbkOil & GasOil & Gas Production & RefineryEnergy0.000.00.038.78880.0164257543161511
10PGAS.JKPT Perusahaan Gas Negara Tbk.Oil & GasOil & Gas Storage & DistributionEnergy211.6582292000000000.1907914635604840.0309451120764361141.0540.0477137375622988
11PTPP.JKPP (Persero) TbkHeavy Constructions & Civil EngineeringHeavy Constructions & Civil EngineeringInfrastructures0.025903000000000-1.00.29554348428994333.8420.0114528350532055
12SMGR.JKSemen Indonesia (Persero) TbkConstruction MaterialsConstruction MaterialsBasic Materials390.38411877000000000.2143146260558410.0656209768556656245.190.0204733341000974
13SSIA.JKPT Surya Semesta Internusa TbkHeavy Constructions & Civil EngineeringHeavy Constructions & Civil EngineeringInfrastructures94.9855643600000001.447881908486690.2262636621863435.00.00422613676637411
14TBIG.JKPT Tower Bersama Infrastructure TbkWireless Telecommunication ServicesWireless Telecommunication ServicesInfrastructures75.727076440000000NaN0.065625402351729460.34550.0212685994803905
15TLKM.JKPT Telkom Indonesia (Persero) TbkTelecommunication ServiceIntegrated Telecommunication ServiceInfrastructures281.351611710000000000.1409978812395210.0801187540210165167.5990.037667010165751
16TOWR.JKSarana Menara Nusantara TbkWireless Telecommunication ServicesWireless Telecommunication ServicesInfrastructures71.6912786000000000NaN0.089065099875685124.10.0254231836646795
17UNTR.JKUnited Tractors TbkMachineryConstruction Machinery & Heavy VehiclesIndustrials4274.33116960000000000NaN-0.09039484329780271569.00.0932280221953988

2. Exploratory Data Analysis (EDA)

Our goal is to select two or three stocks from a list of 18 for Indra to invest in.

At the beginning of this section, we outlined three strategies:

  • Diversification by Industry and Sector: Conduct an overview analysis to understand the information better.
  • Diversification by Correlation: Examine the correlation matrix based on stock price evolution.
  • Company Growth and Dividend Information: Extract all relevant information beyond sector and industry, normalize the data, and assign a score to each stock.

Overview of Sectors and Industries

# Categorize stocks by industries
df_sminfra18_company_report.industry.value_counts()
industrycount
Wireless Telecommunication Services4
Heavy Constructions & Civil Engineering3
Oil & Gas3
Banks3
Construction Materials2
Transport Infrastructure Operator1
Telecommunication Service1
Machinery1
# Categorize stocks by sectors
df_sminfra18_company_report.sector.value_counts()
sectorcount
Infrastructures9
Energy3
Financials3
Basic Materials2
Industrials1

Correlation Matrix on stock price evolution

# Calculate the correlation of the 18 stock prices
correlation_matrix = df_pivot_history_sminfra18.corr()
correlation_matrix
symbolADHI.JKAKRA.JKBBNI.JKBBRI.JKBMRI.JKEXCL.JKINTP.JKISAT.JKJSMR.JKMEDC.JKPGAS.JKPTPP.JKSMGR.JKSSIA.JKTBIG.JKTLKM.JKTOWR.JKUNTR.JK
ADHI.JK1.00.44054831497766950.176121144846044270.7928442943673347-0.26118245240557680.71241462248543990.51797180120405460.5329360798039315-0.394091475218512230.33528078617669150.447768312190643670.95009487246415810.69871628822444310.407847237629603730.352943847096223950.7749442034319914-0.1865431914048376-0.44114813721718016
AKRA.JK0.44054831497766951.00.0139118058906017360.6413359452021958-0.402468608382517250.42943642758764520.61808418427826560.6941343306422824-0.097228912008373430.61169993732819380.59614139902556350.58089492762832510.68801711353950750.78381760260874460.51930567020724620.5911278263781223-0.50240099689572160.19039720779418942
BBNI.JK0.176121144846044270.0139118058906017361.00.098946026905068860.5636631033318196-0.2607215462091407-0.24621304704373226-0.039457237584926720.38445415918585185-0.244485034043916750.442136333378867650.18577949412005265-0.0383043997430906750.098078740020994810.01675141790049434-0.1315229597947040.3683855137605620.4890767740162628
BBRI.JK0.79284429436733470.64133594520219580.098946026905068861.0-0.359643202717533640.71316112517860120.63773606193813560.6944768316021509-0.368199734758251130.480077347803790240.419695776621881070.82398945778576770.8497826495851870.53296253335156380.36034577032598870.8355662492715948-0.48555082430184326-0.2679599156589215
BMRI.JK-0.2611824524055768-0.402468608382517250.5636631033318196-0.359643202717533641.0-0.5506463859261369-0.7520183717141449-0.58222381506410690.2976567150382238-0.12157317317982577-0.12551088987459938-0.3273249689789752-0.6433056090140505-0.1540243168521433-0.3572442319395516-0.60917105041939690.72637319754915990.5449126714206115
EXCL.JK0.71241462248543990.4294364275876452-0.26072154620914070.7131611251786012-0.55064638592613691.00.65534521259592820.5940616942295305-0.369504819960814970.367539228567921870.178426003438631660.73520564036411560.74640202750790720.46126855912160820.26712080591955430.8079859959380536-0.3481709027858281-0.6040120596524621
INTP.JK0.51797180120405460.6180841842782656-0.246213047043732260.6377360619381356-0.75201837171414490.65534521259592821.00.6678626435917601-0.34150997003756340.29796955199579530.258130036708675070.64955227827049460.80750039250389580.31681926095399920.70546297858644040.765074027229533-0.611387813599545-0.37164800207902904
ISAT.JK0.53293607980393150.6941343306422824-0.039457237584926720.6944768316021509-0.58222381506410690.59406169422953050.66786264359176011.0-0.177364883722500060.156892753832064020.5996438339642450.63650339022124310.88527727508878270.450633136082693730.50514799964481570.8128682498730978-0.7780482202400613-0.17936652807654763
JSMR.JK-0.39409147521851223-0.097228912008373430.38445415918585185-0.368199734758251130.2976567150382238-0.36950481996081497-0.3415099700375634-0.177364883722500061.0-0.240082391764027570.006762152980253546-0.3669961996486978-0.33394064071886470.17644122568390785-0.04021797493336236-0.440506488555726160.252760267058337040.5656059851195705
MEDC.JK0.33528078617669150.6116999373281938-0.244485034043916750.48007734780379024-0.121573173179825770.367539228567921870.29796955199579530.15689275383206402-0.240082391764027571.00.19263746457410120.3796654629597170.24034659907661110.59107424629746650.283597574264435940.24831867087132173-0.045727216893117380.0714817282407066
PGAS.JK0.447768312190643670.59614139902556350.442136333378867650.41969577662188107-0.125510889874599380.178426003438631660.258130036708675070.5996438339642450.0067621529802535460.19263746457410121.00.56684228160105210.48107250437460490.475448562100340640.45798839390845280.3606945742702592-0.23510732310291520.2807642116842334
PTPP.JK0.95009487246415810.58089492762832510.185779494120052650.8239894577857677-0.32732496897897520.73520564036411560.64955227827049460.6365033902212431-0.36699619964869780.3796654629597170.56684228160105211.00.79488615815434330.465764924443565930.54093138843480980.8139582360893746-0.2645797556689458-0.3187692504527025
SMGR.JK0.69871628822444310.6880171135395075-0.0383043997430906750.849782649585187-0.64330560901405050.74640202750790720.80750039250389580.8852772750887827-0.33394064071886470.24034659907661110.48107250437460490.79488615815434331.00.414844857119669060.50219530222322830.9333560226628546-0.731329490242146-0.3318282333603005
SSIA.JK0.407847237629603730.78381760260874460.098078740020994810.5329625333515638-0.15402431685214330.46126855912160820.31681926095399920.450633136082693730.176441225683907850.59107424629746650.475448562100340640.465764924443565930.414844857119669061.00.2575336644156190.4031179951238351-0.12348040987679690.1928933786126617
TBIG.JK0.352943847096223950.51930567020724620.016751417900494340.3603457703259887-0.35724423193955160.26712080591955430.70546297858644040.5051479996448157-0.040217974933362360.283597574264435940.45798839390845280.54093138843480980.50219530222322830.2575336644156191.00.4481306404943432-0.34270414412057320.047036835947018116
TLKM.JK0.77494420343199140.5911278263781223-0.1315229597947040.8355662492715948-0.60917105041939690.80798599593805360.7650740272295330.8128682498730978-0.440506488555726160.248318670871321730.36069457427025920.81395823608937460.93335602266285460.40311799512383510.44813064049434321.0-0.6245523636918808-0.5185881293077921
TOWR.JK-0.1865431914048376-0.50240099689572160.368385513760562-0.485550824301843260.7263731975491599-0.3481709027858281-0.611387813599545-0.77804822024006130.25276026705833704-0.04572721689311738-0.2351073231029152-0.2645797556689458-0.731329490242146-0.1234804098767969-0.3427041441205732-0.62455236369188081.00.2112746039676902
UNTR.JK-0.441148137217180160.190397207794189420.4890767740162628-0.26795991565892150.5449126714206115-0.6040120596524621-0.37164800207902904-0.179366528076547630.56560598511957050.07148172824070660.2807642116842334-0.3187692504527025-0.33182823336030050.19289337861266170.047036835947018116-0.51858812930779210.21127460396769021.0

3. Normalization & Scoring

We will extract all relevant columns (excluding those related to sectors and industries) from the df_sminfra18_company_report to evaluate the growth potential and dividend information for the 18 companies. Based on this analysis, we will assign scores to each company to identify the top candidates. Subsequently, we will validate our selections through diversification considerations concerning industry and sector, as well as correlation analysis.

# Import library for normalization
from sklearn.preprocessing import MinMaxScaler

# Extract useful information for normalization analysis
columns_of_interest = ['symbol', 'eps_growth', 'avg_yield_dividends', 'revenue_growth', 'revenue_estimate']
df_sminfra18_company_report_norm = df_sminfra18_company_report[columns_of_interest]
df_sminfra18_company_report_norm
indexsymboleps_growthavg_yield_dividendsrevenue_growthrevenue_estimatescore
0ADHI.JK0.00.054676448921071420.00.073224634122384840.12790108304345627
1AKRA.JK0.461009839189913730.49604749421835690.56407566678098930.21264821025158911.7337812104408492
2BBNI.JK0.4943244077353830.53359134649636390.71938400611800520.334974111440077442.0822738717898295
3BBRI.JK0.46302090830158970.376161197045529470.94728625089211470.99999999999999992.786468356239234
4BMRI.JK0.45536353410414930.44159585971133491.00.75643706063114762.6533964544466317
5EXCL.JK0.68646366387319080.086169626612896930.66922892241074040.171777026300157621.6136392391969858
6INTP.JK0.37383014555006510.379101081547317360.6105446862135510.092769912208081341.4562458255190147
7ISAT.JK0.58785842915982460.403373985476873750.68384343237485720.274117052773373341.949192899784929
8JSMR.JK0.180174443618182470.024034427616687970.122050855244210280.08424796890566460.41050769538474535
9MEDC.JK0.40851643885803790.137071450688600050.41334794136475760.00.9589358309113956
10PGAS.JK0.486457888116279940.48861437694602830.47147041234153710.27235232763176971.718895005035615
11PTPP.JK0.00.081197137026882220.96845071641978430.121154718640230861.1708025720868973
12SMGR.JK0.496067486689562130.18254891180569150.53660014718454670.192645029723902131.4078615754037025
13SSIA.JK1.00.00.83832630647915350.0260258838826759461.8643521903618294
14TBIG.JK0.43728023842564760.19148428858405530.53660845934595980.033098254919294111.1984712412749567
15TLKM.JK0.46611639118853560.375732190820211250.56383051010711560.7538365115223972.1595156036382597
16TOWR.JK0.43728023842564760.238164020864582220.58063393049873730.0598032750080682431.3158814647970354
17UNTR.JK0.43728023842564761.00000000000000020.243564369631377730.54705076215733322.227895370214359
# Check missing data
df_sminfra18_company_report_norm.isnull().sum()
index0
symbol0
eps_growth3
avg_yield_dividends0
revenue_growth0
revenue_estimate0
# Impute missing data with mean - we do a simple imputation here, can also check the reason and correlation with other variables behind missing data
df_sminfra18_company_report_norm.fillna(df_sminfra18_company_report_norm.eps_growth.mean(), inplace=True)
df_sminfra18_company_report_norm.isnull().sum()
# As result, all variable should show 0 missing 
# Normalize the data for the metrics using MinMaxScaler
scaler = MinMaxScaler()
df_sminfra18_company_report_norm[['eps_growth', 'avg_yield_dividends', 'revenue_growth', 'revenue_estimate']] = scaler.fit_transform(df_sminfra18_company_report_norm[['eps_growth', 'avg_yield_dividends', 'revenue_growth', 'revenue_estimate']])
# Calculate the score by summing the normalized scores for all metrics
df_sminfra18_company_report_norm['score'] = df_sminfra18_company_report_norm.iloc[:, 1:5].sum(axis=1)

# Reorder columns to place 'score' as the third column, after 'symbol' and before 'eps_growth'
columns = list(df_sminfra18_company_report_norm.columns)
columns.insert(1, columns.pop(columns.index('score')))
df_sminfra18_company_report_norm = df_sminfra18_company_report_norm[columns]

# Display the DataFrame
df_sminfra18_company_report_norm
indexsymbolscoreeps_growthavg_yield_dividendsrevenue_growthrevenue_estimate
0BBRI.JK4.5729367124784680.46302090830158970.376161197045529470.94728625089211470.9999999999999999
1BMRI.JK4.5503558482621160.45536353410414930.44159585971133491.00.7564370606311476
2UNTR.JK3.9087399782713840.43728023842564761.00000000000000020.243564369631377730.5470507621573332
3TLKM.JK3.56519469575412230.46611639118853560.375732190820211250.56383051010711560.753836511522397
4BBNI.JK3.82957363213958150.4943244077353830.53359134649636390.71938400611800520.33497411144007744
5ISAT.JK3.62426874679648450.58785842915982460.403373985476873750.68384343237485720.27411705277337334
6SSIA.JK3.7026784968409831.00.00.83832630647915350.026025883882675946
7AKRA.JK3.2549142106301090.461009839189913730.49604749421835690.56407566678098930.2126482102515891
8PGAS.JK3.16543768243946030.486457888116279940.48861437694602830.47147041234153710.2723523276317697
9EXCL.JK3.05550145209381440.68646366387319080.086169626612896930.66922892241074040.17177702630015762
10INTP.JK2.8197217388299480.37383014555006510.379101081547317360.6105446862135510.09276991220808134
11SMGR.JK2.6230781210835030.496067486689562130.18254891180569150.53660014718454670.19264502972390213
12TOWR.JK2.57195965458600240.43728023842564760.238164020864582220.58063393049873730.059803275008068243
13TBIG.JK2.36384422763061950.43728023842564760.19148428858405530.53660845934595980.03309825491929411
14PTPP.JK2.2204504255335640.00.081197137026882220.96845071641978430.12115471864023086
15MEDC.JK1.91787166182279110.40851643885803790.137071450688600050.41334794136475760.0
16JSMR.JK0.7367674218638260.180174443618182470.024034427616687970.122050855244210280.0842479689056646
17ADHI.JK0.18257753196452770.00.054676448921071420.00.07322463412238484
# Sort the companies by total score
df_sminfra18_company_report_norm = df_sminfra18_company_report_norm.sort_values(by='score', ascending=False).reset_index(drop=True)
df_sminfra18_company_report_norm
indexsymboleps_growthavg_yield_dividendsrevenue_growthrevenue_estimatescore
0BBRI.JK0.46302090830158970.376161197045529470.94728625089211470.99999999999999992.786468356239234
1BMRI.JK0.45536353410414930.44159585971133491.00.75643706063114762.6533964544466317
2UNTR.JK0.43728023842564761.00000000000000020.243564369631377730.54705076215733322.227895370214359
3TLKM.JK0.46611639118853560.375732190820211250.56383051010711560.7538365115223972.1595156036382597
4BBNI.JK0.4943244077353830.53359134649636390.71938400611800520.334974111440077442.0822738717898295
5ISAT.JK0.58785842915982460.403373985476873750.68384343237485720.274117052773373341.949192899784929
6SSIA.JK1.00.00.83832630647915350.0260258838826759461.8643521903618294
7AKRA.JK0.461009839189913730.49604749421835690.56407566678098930.21264821025158911.7337812104408492
8PGAS.JK0.486457888116279940.48861437694602830.47147041234153710.27235232763176971.718895005035615
9EXCL.JK0.68646366387319080.086169626612896930.66922892241074040.171777026300157621.6136392391969858
10INTP.JK0.37383014555006510.379101081547317360.6105446862135510.092769912208081341.4562458255190147
11SMGR.JK0.496067486689562130.18254891180569150.53660014718454670.192645029723902131.4078615754037025
12TOWR.JK0.43728023842564760.238164020864582220.58063393049873730.0598032750080682431.3158814647970354
13TBIG.JK0.43728023842564760.19148428858405530.53660845934595980.033098254919294111.1984712412749567
14PTPP.JK0.00.081197137026882220.96845071641978430.121154718640230861.1708025720868973
15MEDC.JK0.40851643885803790.137071450688600050.41334794136475760.00.9589358309113956
16JSMR.JK0.180174443618182470.024034427616687970.122050855244210280.08424796890566460.41050769538474535
17ADHI.JK0.00.054676448921071420.00.073224634122384840.12790108304345627
# Pick the top 5 companies
top_5_companies = df_sminfra18_company_report_norm.head(5)
top_5_companies
indexsymboleps_growthavg_yield_dividendsrevenue_growthrevenue_estimatescore
0BBRI.JK0.46302090830158970.376161197045529470.94728625089211470.99999999999999992.786468356239234
1BMRI.JK0.45536353410414930.44159585971133491.00.75643706063114762.6533964544466317
2UNTR.JK0.43728023842564761.00000000000000020.243564369631377730.54705076215733322.227895370214359
3TLKM.JK0.46611639118853560.375732190820211250.56383051010711560.7538365115223972.1595156036382597
4BBNI.JK0.4943244077353830.53359134649636390.71938400611800520.334974111440077442.0822738717898295

4. Stock selection choice

Based on the results from the section above, we have identified the top five stock tickers that we should consider for selection, as they have received the highest scores.

Next, we will implement our diversification strategy, focusing on industry and sector, as well as correlation analysis, to select two stocks from this group of five high-scoring candidates.

# Store the top 5 company symbol into a list
top_5_symbols = top_5_companies['symbol'].tolist()
top_5_symbols

[‘BBRI.JK’, ‘BMRI.JK’, ‘UNTR.JK’, ‘TLKM.JK’, ‘BBNI.JK’]

# Query the sector & industry information for the top 5 companies
top_5_info = df_sminfra18_company_report[df_sminfra18_company_report['symbol'].isin(top_5_symbols)]
top_5_info
indexsymbolcompany_nameindustrysub_industrysectoreps_estimaterevenue_estimateeps_growthrevenue_growthtotal_dividendsavg_yield_dividends
2BBNI.JKPT Bank Negara Indonesia (Persero) TbkBanksBanksFinancials679.11716178000000000.2100477746188420.162937331535753280.4950.0517167726531625
3BBRI.JKPT Bank Rakyat Indonesia (Persero) TbkBanksBanksFinancials451.372138010000000000.1334205046825360.284275264842541319.00.0377051925286651
4BMRI.JKPT Bank Mandiri (Persero) TbkBanksBanksFinancials657.581617270000000000.1146761569181090.312340707462218353.9580.043529000878334
15TLKM.JKPT Telkom Indonesia (Persero) TbkTelecommunication ServiceIntegrated Telecommunication ServiceInfrastructures281.351611710000000000.1409978812395210.0801187540210165167.5990.037667010165751
17UNTR.JKUnited Tractors TbkMachineryConstruction Machinery & Heavy VehiclesIndustrials4274.33116960000000000NaN-0.09039484329780271569.00.0932280221953988

We have three stocks are all in banking industry / financial sector. For those three tickers (BBNI.JK, BBRI.JK, BMRI.JK), we choose the ticker with the highest score - BBRI.JK.

Now we have our choice: BBRI.JK, TLKM.JK, UNTR.JK.

We need to confirm that their price movement doesn’t have high positive correlation.

# Query the price history info for selected 3 tickers and check correlation
selected_3_symbols =  ['BBRI.JK', 'TLKM.JK', 'UNTR.JK']
selected_3_prices = df_pivot_history_sminfra18[selected_3_symbols]
correlation_matrix_selected = selected_3_prices.corr()
correlation_matrix_selected
symbolBBRI.JKTLKM.JKUNTR.JK
BBRI.JK1.00.8355662492715948-0.2679599156589215
TLKM.JK0.83556624927159481.0-0.5185881293077921
UNTR.JK-0.2679599156589215-0.51858812930779211.0

TLKM.JK and BBRI.JK, their price has strong positive correlation! We drop one and only select BBRI.JK, since it has the highest score regarding growth potential analysis.

To sum up, our final choices are two stocks: BBRI.JK & UNTR.JK.

Here is a brief analysis of Indra’s stock performance.

# Normalize prices to start from 100 for easy comparison
price_norm = price_Indra / price_Indra.iloc[0] * 100

# Calculate percentage gains from the initial price
price_gains = ((price_Indra - price_Indra.iloc[0]) / price_Indra.iloc[0]) * 100

# Plotting normalized prices and percentage gains
plt.figure(figsize=(14, 6))

# Plot normalized prices
plt.subplot(1, 2, 1)
price_norm.plot(ax=plt.gca(), title="Normalized Price Performance")
plt.ylabel('Normalized Price (Start = 100)')
plt.xlabel('Date')
plt.legend(title='Stock Symbol')
plt.grid(True)

# Plot percentage gains
plt.subplot(1, 2, 2)
price_gains.plot(ax=plt.gca(), title="Percentage Gains Over Time")
plt.ylabel('Percentage Gain (%)')
plt.xlabel('Date')
plt.legend(title='Stock Symbol')
plt.grid(True)

plt.tight_layout()
plt.show()

MVO with Monte Carlos Simulation

After understanding which stock to invest in, here are some key questions:

  • How much of each stock should we hold?
  • How to balance between risk and reward?

This is where Mean-Variance Optimization (MVO) comes in. It’s a method to help us find the optimal way to allocate our capital among various stocks - find the optimal weight.

Step 1: Gather useful information

What are the information we need?

  1. Expected Returns: This is the average return you expect from each stock based on historical performance.

  2. Risk (Volatility): Risk is measured as the variance (or more commonly, standard deviation) of the returns. It shows how much the stock’s returns fluctuate.

  3. Covariance Between Stocks: Covariance tells us how the returns of two stocks move together. Some stocks may go up and down at the same time (positive covariance), while others may move in opposite directions (negative covariance). Covariance helps us understand how diversification can reduce risk.

Step 2: Build the Model

Once we have this data, the next step is to use it to construct a portfolio of stocks. The goal of the model is twofold:

  • Maximize Return: We want to achieve the highest possible return for a given amount of risk.
  • Minimize Risk: Alternatively, we might want to minimize risk while achieving a certain return level.

Consult the mathematical model here: link

Step 3: Optimize the Portfolio through Sharp Ratio

Now that the model is built, we can use it to optimize the portfolio. This means:

  • Maximizing Return for a Given Level of Risk: This is suitable for investors who are willing to take on some risk but want to get the highest return for that risk level.

  • Minimizing Risk for a Given Return: This is helpful for more conservative investors who want to reduce risk as much as possible while still achieving a certain return.

This optimization results will generate an Efficient Frontier:

A curve that shows the best possible portfolios in terms of risk and return. Every point on the frontier represents a portfolio that either minimizes risk for a given return or maximizes return for a given risk.

The highest sharp ratio point, which lies on the efficient frontier, gives the optimal portfolio weight.

Bibliography:

Useful math for Portfolio Optimization. link

Understand basic concepts of mean (return), variance (risk) and how they work in investing. link

1. Mean & Variance

For this section, for a better illustration of the model, we will randomly generate 4 stocks from the sminfra18 to compose a portfolio to optimize.

# Import all useful libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Randomly select 4 stocks from sminfra18
sminfra18_tickers = df_pivot_history_sminfra18.columns.tolist()

selected_stocks = np.random.choice(sminfra18_tickers, size=4, replace=False)
selected_stocks
array(['PGAS.JK', 'UNTR.JK', 'TBIG.JK', 'TLKM.JK'], dtype='<U7')
# Query the price info for the selected stocks
prices = df_pivot_history_sminfra18[selected_stocks]
prices.head()
datePGAS.JKUNTR.JKTBIG.JKTLKM.JK
2024-07-10 00:00:001520.023550.01935.03160.0
2024-07-11 00:00:001560.023500.01990.03180.0
2024-07-12 00:00:001580.023500.02000.03220.0
2024-07-15 00:00:001580.023750.01970.03160.0
2024-07-16 00:00:001610.023800.02010.03100.0
# Calculate Returns
returns = pd.DataFrame()

for stock in prices:
  returns[stock + ' Returns'] = prices[stock].pct_change().dropna()

returns.head()
datePGAS.JK ReturnsUNTR.JK ReturnsTBIG.JK ReturnsTLKM.JK Returns
2024-07-11 00:00:000.026315789473684292-0.0021231422505307850.0284237726098190980.006329113924050667
2024-07-12 00:00:000.0128205128205127750.00.0050251256281406140.012578616352201255
2024-07-15 00:00:000.00.010638297872340496-0.015000000000000013-0.018633540372670843
2024-07-16 00:00:000.0189873417721520.0021052631578948320.020304568527918843-0.01898734177215189
2024-07-17 00:00:000.0062111801242235030.0-0.009950248756218860.048387096774193505
# Calculate expected returns (mean) and covariance matrix
# Assuming 252 is the trading days in a year = 365 - weekends 104 - holidays 9
# We need to annualize the return and risk since MVO models are long-term orientated and long-term in economics perspectives refers to 12 months +

expected_returns = returns.mean() * 252  # Annualize by multiplying by trading days
covariance_matrix = returns.cov() * 252   # Annualize covariance

# Print the expected returns and covariance matrix
print("Expected Returns:")
print(expected_returns)
print("\nCovariance Matrix:")
print(covariance_matrix)
Expected Returns:
PGAS.JK Returns    0.394443
UNTR.JK Returns    0.582308
TBIG.JK Returns   -0.056850
TLKM.JK Returns   -1.207224
dtype: float64

Covariance Matrix:
                 PGAS.JK Returns  UNTR.JK Returns  TBIG.JK Returns  \
PGAS.JK Returns         0.048921         0.022824         0.002496   
UNTR.JK Returns         0.022824         0.074070         0.008815   
TBIG.JK Returns         0.002496         0.008815         0.067872   
TLKM.JK Returns         0.013206        -0.006651         0.018117   

                 TLKM.JK Returns  
PGAS.JK Returns         0.013206  
UNTR.JK Returns        -0.006651  
TBIG.JK Returns         0.018117  
TLKM.JK Returns         0.110532 
# Here we can see the distribution of each stock returns
returns.hist(figsize=(8,6), bins = 50)
plt.show()

2. Monte Carlo Simulation

Monte Carlo simulation is a powerful technique used in portfolio optimization to assess the potential outcomes of different investment strategies or different allocations under varying conditions. It involves generating multiple scenarios based on statistical models and random sampling.

Implementing Monte Carlo simulation in Python involves combining statistical analysis, simulation, and optimization techniques to gain insights into portfolio performance under different allocations. For our analysis, we will run a simulation on different allocations of the same stocks to find the optimum allocation. A single run of the simulation is shown in the code below.

# Define Risk Free rate
# Assume RF as 6.64% as the yield of 10 year Indonesian Government bond
# Source link https://tradingeconomics.com/indonesia/government-bond-yield
rf = 0.0664

# Monte Carlo simulation
#Single Run
np.random.seed(101)
print(selected_stocks)

# Generates an array of random numbers representing initial weights for each asset in the portfolio
weights = np.array(np.random.random(4))
print('\nRandom Weights')
print(weights)

# Normalizing the randomly generated weights to ensure they sum up to 1, representing a fully invested portfolio.
print('\nRebalanced Weights')
weights = weights / np.sum(weights)
print(weights)

# Calculating the portfolio’s expected return using the weighted average of individual asset returns.
# It multiplies the annual mean returns of each asset by its respective weight and aggregates them.
print('\nPortfolio Return')
portfolio_return = np.sum(returns.mean()*252*weights)
print(portfolio_return)

# Expected Volatility (Standard Deviation): Using the formula for portfolio volatility, it calculates the square root of the dot product of weights, the covariance matrix of asset returns (multiplied by 252 for annualization), and weights transpose.
# The square root of covariance matrix is taken because square root of variance is standard deviation or volatility.
print('\nPortfolio Volatility')
portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov()*252, weights)))
print(portfolio_volatility)

# Sharpe Ratio measures the risk-adjusted return by subtracting the risk-free rate from the portfolio return and dividing by its volatility.
print('\nPortfolio Sharpe Ratio')
sharpe_ratio = (portfolio_return - rf) / portfolio_volatility
print(sharpe_ratio)
['PGAS.JK' 'UNTR.JK' 'TBIG.JK' 'TLKM.JK']

Random Weights
[0.51639863 0.57066759 0.02847423 0.17152166]

Rebalanced Weights
[0.40122278 0.44338777 0.02212343 0.13326603]

Portfolio Return
0.25430815938385554

Portfolio Volatility
0.18304080881645118

Portfolio Sharpe Ratio
1.0265916141808857

Having seen a single run of the simulation above, let us now perform 5000 simulations of the random allocations generated to find the optimum allocation for the four stocks chosen. The portfolio performance will analyzed based on the Sharpe ratio. The Sharpe ratio gives the return delivered per unit of risk taken. The code for the simulation is shown below.

# Specify the number of simulated portfolios to generate (in our case, 5000)
num_ports = 5000

# A 2D array to store the randomly generated weights for each asset in each portfolio.
all_weights = np.zeros((num_ports, len(selected_stocks)))

# Create arrays to store portfolio returns, volatilities, and Sharpe ratios for each simulated portfolio.
ret_arr = np.zeros(num_ports)
vol_arr = np.zeros(num_ports)
sharp_arr = np.zeros(num_ports)

# Looping over each portfolio
for i in range(num_ports):

  # Generate random weights randomly assigning weights to each asset in the portfolio and rebalancing them to sum up to 1
  weights = np.array(np.random.random(4))
  weights = weights / np.sum(weights)
  all_weights[i, :] = weights

  #Expected return
  ret_arr[i] = np.sum((returns.mean() * 252 * weights))

  #Expected volatility
  vol_arr[i] = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))

  #Sharpe ratio
  sharp_arr[i] = (ret_arr[i] - rf) / vol_arr[i]

3. Sharpe Ratio

Sharpe ratio measures the risk-adjusted return by subtracting the risk-free rate. We want the portfolio weight randomly generated by Monte Carlo Simulation with the highest sharp ratio.

See more here - link

# Retrieve the maximum value from the sharp_arr array
max_sr = sharp_arr.max()

# Find the index of the portfolio with the maximum Sharpe ratio in the sharp_arr array.
max_sr_index = sharp_arr.argmax()

# Retrieve the weights of the assets in the portfolio corresponding to the index 4091 in the all_weights array.
opt_weights = all_weights[max_sr_index,:]

# Retrive the optimal expected returns and volaticity
optimal_return = ret_arr[max_sr_index]
optimal_volatility = vol_arr[max_sr_index]

# Print Result
print('Max Sharpe Ratio: ', max_sr)
print('Optimal Return: ', optimal_return)
print('Optimal Volatility: ', optimal_volatility)
print('Optimal Weights: ', opt_weights)
print('Stock List: ', selected_stocks)
Max Sharpe Ratio:  2.0346018098713943
Optimal Return:  0.4819911385679692
Optimal Volatility:  0.20426165776105273
Optimal Weights:  [0.47205837 0.51425067 0.01118475 0.00250621]
Stock List:  ['PGAS.JK' 'UNTR.JK' 'TBIG.JK' 'TLKM.JK']

4. Efficient Frontier

The efficient frontier rates portfolios on a coordinate plane. Plotted on the x-axis is the risk, while return is plotted on the y-axis—annualized standard deviation is typically used to measure risk, while compound annual growth rate (CAGR) is used for return.

The volatility, return and sharpe ratio values for the simulation are plotted below.

plt.figure(figsize = (10,5))
plt.scatter(vol_arr, ret_arr, c = sharp_arr, cmap = 'plasma')
plt.colorbar(label = 'Sharpe Ratio')
plt.xlabel('Volatility', fontweight = 'bold')
plt.ylabel('Return', fontweight = 'bold')

plt.scatter(optimal_volatility, optimal_return, c='red', s=200, edgecolors='black', marker='*')
plt.grid(True, ls=':', lw=1)

The visualization can be seen above for volatility, return and sharpe ratio. The x-axis shows the volatility, y axis shows the return and the colorbar on right shows the sharpe ratio graded colorwise with dark color showing the lowest and light values showing the highest sharpe ratio. The red star shows the highest Sharpe ratio point on the plot.

MVO with Scipy Minimized Function

1. Overview on the method

In our next analysis method, we will optimize the same portfolio allocation mathematically using the minimize function in Scipy (a library in Python) and Sharpe ratio.

Portfolio optimization using Scipy’s minimize function and the Sharpe ratio involves using mathematical optimization to find the optimal asset allocation that maximizes the Sharpe ratio—a measure of risk-adjusted returns.

The basic principle is to find the Sharpe ratio for a random allocation and then multiply it by -1 to make it negative and then minimize it to obtain the allocation weights that gives the highest Sharpe ratio.

def ret_vol_sr(weights):
  weights = np.array(weights)

  # Calculate Annualized Expected Returns
  ret = np.sum(returns.mean() * weights * 252)

  # Calculate Portfolio Volaticity
  vol = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))

  # Calculate Sharp Ratio, Rist Free rate 6.4%
  sr = (ret - rf) / vol

  return np.array([ret, vol, sr])

2. Scipy Minimize Function


from scipy.optimize import minimize

# This function below is designed to negate the Sharpe ratio because in optimization, the objective function is minimized by default.
# We want to maximize the Sharpe ratio, index 2, so multiplying by -1 helps flip the optimization to achieve that.
def sharp_neg(weights):
  return ret_vol_sr(weights)[2] * -1

# constraint function ensures the weight sum is 1
# In optimization, constraints are typically written such that the result equals 0
# If the sum of weights is exactly 1, then np.sum(weights) - 1 will equal 0. This satisfies the constraint that the sum of the weights must be 1.
def sum_check(weights):
  return np.sum(weights) - 1

# We want equity constraint, the function must return a value equals to 0
# Specify the equality constraint that the sum of weights must equal 1.
cons = ({'type': 'eq', 'fun': sum_check})

# Ensure that each weight lies between 0 and 1, meaning no short-selling or leverage.
bounds = ((0, 1), (0, 1), (0, 1), (0, 1))

# Initial guess for the optimization equal weighting of 25% for each stock initially.
init_guess = [0.25, 0.25, 0.25, 0.25]

# SLSQP stands for Sequential Least Squares Quadratic Programming, which is an algorithm that can handle both equality and inequality constraints, making it well-suited for portfolio optimization problems.
# We are minimize the negative sharp ratio - maximize sharp ratio

opt_results = minimize(sharp_neg, init_guess, method = 'SLSQP', bounds = bounds, constraints = cons)

opt_results

fun —> Returns the result for the negative sharpe ratio x —> Represents the optimizal weights in accordance with the stock list order nit —> This value indicates the number of iterations that the optimization algorithm took to converge to the optimal solution. A smaller number usually means a faster convergence jac —> Jacobian (or gradient) of the objective function at the optimal weights

message: Optimization terminated successfully
 success: True
  status: 0
     fun: -2.0678015196575585
       x: [ 4.136e-01  5.864e-01  0.000e+00  1.062e-14]
     nit: 6
     jac: [-3.133e-01 -3.133e-01  5.538e-01  5.769e+00]
    nfev: 31
    njev: 6
# opt_results.x gives the optimized portfolio weights.
opt_results.x
array([4.13647417e-01, 5.86352583e-01, 0.00000000e+00, 1.06241346e-14])

The array represents the weights of assets in an optimal portfolio, likely derived from a process such as mean-variance optimization or another portfolio optimization method. Here’s what the weights signify:

  • 0.4136 (or 41.36%) is allocated to the first asset.
  • 0.5864 (or 58.64%) is allocated to the second asset.
  • 0.0000 (or 0%) is allocated to the third asset.
  • 1.06e-14 (or essentially 0%) is allocated to the fourth asset.
#return the expected return, volatility, and Sharpe ratio for the optimized portfolio.
ret_vol_sr(opt_results.x)
array([0.50459828, 0.21191506, 2.06780152])
# Print Result
print('Return: ', ret_vol_sr(opt_results.x)[0])
print('Volatility: ', ret_vol_sr(opt_results.x)[1])
print('Sharpe Ratio: ', ret_vol_sr(opt_results.x)[2])
print (selected_stocks)
print ('Optimal Weights: ', opt_results.x)
Return:  0.5045982793617794
Volatility:  0.21191505818911857
Sharpe Ratio:  2.0678015196575585
['PGAS.JK' 'UNTR.JK' 'TBIG.JK' 'TLKM.JK']
Optimal Weights:  [4.13647417e-01 5.86352583e-01 0.00000000e+00 1.06241346e-14]

3. Result Comparison

# Print Result from Monte Carlos Simulation
print ('\nResult from Monte Carlos Simulation')
print('Stock List: ', selected_stocks)
print('Optimal Weights: ', opt_weights)
print('Optimal Return: ', optimal_return)
print('Optimal Volatility: ', optimal_volatility)
print('Max Sharpe Ratio: ', max_sr)

# Print Result from Scipy Minimized Function
print ('\nResult from Scipy Minimized Function')
print ('Stock List: ', selected_stocks)
print ('Optimal Weights: ', opt_results.x)
print('Return: ', ret_vol_sr(opt_results.x)[0])
print('Volatility: ', ret_vol_sr(opt_results.x)[1])
print('Sharpe Ratio: ', ret_vol_sr(opt_results.x)[2])
Result from Monte Carlos Simulation
Stock List:  ['PGAS.JK' 'UNTR.JK' 'TBIG.JK' 'TLKM.JK']
Optimal Weights:  [0.47205837 0.51425067 0.01118475 0.00250621]
Optimal Return:  0.4819911385679692
Optimal Volatility:  0.20426165776105273
Max Sharpe Ratio:  2.0346018098713943

Result from Scipy Minimized Function
Stock List:  ['PGAS.JK' 'UNTR.JK' 'TBIG.JK' 'TLKM.JK']
Optimal Weights:  [4.13647417e-01 5.86352583e-01 0.00000000e+00 1.06241346e-14]
Return:  0.5045982793617794
Volatility:  0.21191505818911857
Sharpe Ratio:  2.0678015196575585

Let’s use some visualization to compare the results.

# Draw Efficient Frontier

plt.figure(figsize = (10,5))
plt.scatter(vol_arr, ret_arr, c = sharp_arr, cmap = 'plasma')
plt.colorbar(label = 'Sharpe Ratio')
plt.xlabel('Volatility', fontweight = 'bold')
plt.ylabel('Return', fontweight = 'bold')

# Optimial point for Monte Carlos Simulation is labelled as red star
plt.scatter(optimal_volatility, optimal_return, c='red', s=200, edgecolors='black', marker='*')
plt.grid(True, ls=':', lw=1)

# Optimial point for Scipy Optimization is labelled as green star
plt.scatter(ret_vol_sr(opt_results.x)[1], ret_vol_sr(opt_results.x)[0], c='green', s=200, edgecolors='black', marker='*')

Summary:

The Monte Carlo Simulation provides a diversified portfolio with a balanced approach to risk and return, while the Scipy Optimization approach is more aggressive, concentrating investments in fewer stocks with higher expected returns but also slightly increased volatility.

Overall, the Scipy optimization yields better risk-adjusted returns (higher Sharpe ratio) and expected returns, making it a more favorable strategy in this context. However, the Monte Carlo simulation’s results reflect a more cautious approach to diversification that may suit risk-averse investors.

Summary & Recommendation

Workshop Summary

This workshop explored key concepts in portfolio optimization, focusing on sector analysis, stock selection methodologies, and mean-variance optimization (MVO). The main topics included:

  • Understanding Sectors and Stock Indices: Selecting stock indices based on business objectives and industry trends using SectorsAPI. Data Collection and Exploration: Retrieving stock data from APIs and performing exploratory data analysis (EDA) to identify trends and outliers.
  • Stock Selection via Diversification Strategy: Applying EDA and normalization techniques to score companies on growth potential, dividends, and financial indicators.
  • Mean-Variance Optimization (MVO): Implementing portfolio optimization through Monte Carlo simulations and the Scipy minimized function to maximize returns for a given risk level.

Stock Index Selection

In this workshop, we focused on Indra’s case using a single stock index. In practice, building a robust portfolio involves selecting stocks from multiple indices, enhancing diversification across companies, regions, asset classes, and sectors to reduce risk and improve stability. Considerations include global market trends, macroeconomic conditions, and sector performance.

Stock Analysis and Selection

We employed a diversification strategy by normalizing the dataset and scoring companies based on key metrics. In real applications, the analysis would be more complex, incorporating:

  • Macroeconomic Conditions: Factors like interest rates and inflation can significantly impact stock performance.
  • Valuation Metrics: Evaluating P/E ratios, price-to-book, and free cash flow yield is critical for assessing valuations.
  • Risk Factors: Systemic and geopolitical risks should be considered.
  • Sustainability and ESG Factors: Institutional investors increasingly prioritize environmental, social, and governance criteria for long-term growth.

Mean-Variance Optimization

Two approaches to MVO were applied:

  • Monte Carlo Simulation: This method generates thousands of random portfolios to visualize the efficient frontier and calculate the Sharpe ratio. While flexible, it can be computationally expensive and may not converge on the most efficient portfolios.
  • Scipy Minimized Function: This deterministic method computes the optimal portfolio by minimizing volatility or maximizing the Sharpe ratio under specific constraints. It is faster but may be limited by initial guesses for weights and a narrower exploration of possibilities.

Limitations of Both Approaches:

Monte Carlo Limitations: Results depend on iterations, and high computational costs can limit efficiency. Historical returns may not capture future volatility or correlation shifts.

Scipy Optimization Limitations: Sensitivity to initial conditions may lead to local minima, and the assumption of normal return distribution may not hold during extreme market events.

To enhance analysis, consider:

  • Dynamic Risk Modeling: Adjusting risk models for changing correlations and volatilities.
  • Transaction Costs and Liquidity Constraints: Factoring in trading costs and asset liquidity in optimization models.
  • Non-Normal Return Distributions: Accounting for tail risk and skewness in return distributions.