Unit Investment Trust Funds (UITF) are open-ended investment fund pools operated by a trustee (usually a bank) where investors can participate by buying units at the Net Asset Value per Unit (NAVPU) for the day. Mutual Funds (MF), on the other hand, are corporations whose purpose is to invest the fund pool. Investors can participate by buying shares of the corporation at the Net Asset Value per Share (NAVPS). Finally, Exchange-Traded Funds (ETF) are funds traded on the stock exchange (with its own ticker symbol); the ETF itself is invested by the issuer into underlying securities. Investors can participate by buying shares of the ETF on the stock exchange.
Some UITFs and MFs are invested in the money market, some in bonds, and others in equities. In particular, index funds are passive funds which seek to replicate the Philippine Stock Exchange Index (PSEi), which is a basket of 30 common stocks in the PSE. One would then expect the returns to also mimic the PSEi. There are some UITFs and MFs which are index funds. In the Philippines, there is only one ETF at the moment, and it is also an index fund.
This notebook seeks to explore: are all PSEi index funds created equal? Which ones follow the PSEi most faithfully? When price appreciation and dividends of underlying stocks are taken into account, are UITFs / MFs / ETFs still good value for your money?
Index Funds / Indices Analyzed as of April 3, 2020:
Fund / Index Name | Institution | Type |
---|---|---|
BDO Equity Index Fund | BDO Unibank, Inc. | UITF |
BDO PERA Equity Index Fund | BDO Unibank, Inc. | UITF |
BPI Philippine Equity Index Fund | BPI Asset Management and Trust Corporation | UITF |
CTBC Bank - Sun Life Philippines Stock Index Feeder Fund | CTBC Bank (Philippines) Corp. | UITF |
EastWest PSEi Tracker Fund | EastWest Banking Corporation | UITF |
Metro Philippine Equity Index Tracker Fund | Metropolitan Bank & Trust Co. | UITF |
PNB Phil-Index Tracker Fund | Philippine National Bank | UITF |
SB Philippine Equity Index Fund | Security Bank Corporation | UITF |
UnionBank Philippine Equity Index Portfolio | UnionBank | UITF |
UCPB Philippine Index Equity Fund | United Coconut Planters Bank | UITF |
First Metro Save and Learn Philippine Index Fund | First Metro Asset Management, Inc. | MF |
PAMI Equity Index Fund | Philam Asset Management, Inc. | MF |
Philequity PSE Index Fund | Philequity Management, Inc. | MF |
Philippine Stock Index Fund | BPI Investment Management, Inc. | MF |
Sun Life Prosperity Philippine Stock Index Fund | Sun Life Asset Management Company, Inc. | MF |
First Metro Equity Exchange-Traded Fund | First Metro Asset Management, Inc. | ETF |
PSEi | Philippine Stock Exchange | Index |
PSEi Total Return | Philippine Stock Exchange | Index |
Additional Notes on Index Funds Selection and Omission:
From my qualitative analysis of the price data, the following funds which seem to be the best and worst performing are listed below. Otherwise, the rest of the funds are average and more or less track the PSEi closely.
I will try to improve the analysis in the future with updated data and more comparisons. I am trying to replicate how the funds computed the tracking error; it seems I am getting different values, even though I am computing based on the original paper definition here: Determinants of Tracking Error for Equity Portfolios.
There are a lot of equity UITFs and MFs in the Philippines, but only a handful of equity index funds (listed in the table above). I classified UITFs and MFs as index funds if their fund information sheet explicitly notes that they track the index by buying component stocks of the PSEi with the same weight. Index funds are classified as passive funds, since the fund managers do not need to do their due diligence with each individual stock's performance, and only have to ensure the stock weights are as close as possible to the PSEi.
In contrast, all other equity funds can be considered as "active funds" since the fund manager has the discretion to choose different weights, or different stocks altogether. In practice, many equity funds still closely mirror the PSEi (possibly due to liquidity reasons), but they may decide otherwise from time to time. There are other equity funds who focus investments on specific market sectors, dividend plays, or conviction stocks. These active funds may have higher or lower returns than the index depending on the fund managers' skill, luck, and timing, and hence are not the focus of comparison here.
Going back to index funds: how do index funds earn? The PSEi only tracks the prices of the underlying stocks. However, many of these stocks give out dividends to shareholders. When a stock or cash dividend is paid out to shareholders, the stock's price drops the same amount to take into account the "transfer of value". As far as I know, stock dividends are treated similar to stock splits; hence, it is automatically applied to the price history and will have no effect on the PSEi. However, cash dividends are paid directly to shareholders. This amount lowers the PSEi, but the value of cash dividends are not considered when computing the PSEi.
Because of this, investors actually earn in two ways: first, via stock price appreciation (i.e. buy low, sell high); and second, via cash dividends. Since most index funds only track the PSEi's performance, it means that the funds still receive the cash dividends and add these to the Net Asset Value (NAV). This means that they actually have additional earnings which help them track the PSEi.
The PSE has actually tried to address this (similar with other countries) by releasing a new index: the PSEi Total Return Index (PSEi TRI). This index takes into account both the stock prices of the underlying stocks, and the cash dividends distributed by the stocks. Technically, index funds should be tracking the PSEi TRI instead of just the PSEi, but it would make their fund performance look worse. This is also the reason why some index funds like the FMETF beat the PSEi; this is because index funds add the cash dividends to their NAV. When compared against the PSEi TRI, the FMETF is actually underperforming (see analysis in the sections below). But this also means that other index funds are performing even worse.
In practice, although index funds might have additional income from cash dividends, they have additional expenses due to:
Hence, for most funds, the earnings from the dividends actually cancel out the additional expenses incurred by the fund. In practice, this means that most index funds closely follow the PSEi even with the expensive management fees in the Philippines (.
The price data were gathered from each funds' inception date up to April 3, 2020 so far. Raw price data (in JSON format) can be found on my GitHub page: https://github.com/wdjose/pse-index-funds/. These were extracted / downloaded from:
import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup
import json
import os
from datetime import datetime
import matplotlib.pyplot as plt
import matplotlib
from sklearn.metrics import r2_score
# List of fund names
fund_names = [
'BDO Equity Index Fund',
'BDO PERA Equity Index Fund',
'BPI Philippine Equity Index Fund',
'CTBC Bank - Sun Life Philippine Stock Index Feeder Fund',
'EastWest PSEi Tracker Fund',
'Metro Philippine Equity Index Tracker Fund',
'PNB Phil-Index Tracker Fund',
'SB Philippine Equity Index Fund',
'UnionBank Philippine Equity Index Portfolio',
'UCPB Philippine Index Equity Fund',
'First Metro Save and Learn Philippine Index Fund',
'PAMI Equity Index Fund',
'Philequity PSE Index Fund',
'Philippine Stock Index Fund',
'Sun Life Prosperity Philippine Stock Index Fund',
'First Metro Equity Exchange-Traded Fund',
'PSEi',
'PSEi Total Return'
]
# Download JSON files
github_url = 'https://raw.githubusercontent.com/wdjose/pse-index-funds/master/data/{}.json'
fund_series_dict = dict()
for fund in fund_names:
fund_json = requests.get(github_url.format(fund)).json()
df = pd.Series(list(fund_json.values()), index=(fund_json.keys()))
df = df.replace('null', np.nan).apply(pd.to_numeric)
df.index = pd.to_datetime(df.index, format='%b %d, %Y')
fund_series_dict[fund] = df
df = pd.DataFrame(fund_series_dict)
df = df.sort_index()
df = df.dropna(subset=['PSEi'])
df = df.fillna(method='ffill')
# Color generator for perceptually different lines on matplotlib
def my_colors(df):
np.random.seed(0)
n = len(df.columns)
my_colors = np.zeros([n, 3])
my_colors[:,0] = np.remainder(np.linspace(0.35, 1.25, n) + np.random.rand(n)*.05, 1)
my_colors[:,1] = np.random.rand(n) * .1+.9
my_colors[::2,2] = .6
my_colors[1::2,2] = 1
np.random.shuffle(my_colors)
my_colors = matplotlib.colors.hsv_to_rgb(my_colors)
return my_colors
Here, index funds' price performance is computed and plotted together with the benchmark PSEi. The funds' values are normalized at the start of the time period (all funds starting from value=1)
There are four comparisons with varying time scales:
fund_start_date = dict()
for fund in fund_names:
fund_start_date[fund] = df[fund].first_valid_index()
start_date = fund_start_date['PNB Phil-Index Tracker Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = (df_tri_comp[fund] / base - 1) * 100
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Performance of Index Funds vs PSEi (12 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Percent Gain (Loss) from Starting Date')
start_date = fund_start_date['First Metro Equity Exchange-Traded Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = (df_tri_comp[fund] / base - 1) * 100
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Performance of Index Funds vs PSEi (6 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Percent Gain (Loss) from Starting Date')
start_date = fund_start_date['CTBC Bank - Sun Life Philippine Stock Index Feeder Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = (df_tri_comp[fund] / base - 1) * 100
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Performance of Index Funds vs PSEi (3 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Percent Gain (Loss) from Starting Date')
start_date = fund_start_date['PSEi Total Return']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = (df_tri_comp[fund] / base - 1) * 100
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Performance of Index Funds vs PSEi (1 Year)')
ax.set_xlabel('Date')
ax.set_ylabel('Percent Gain (Loss) from Starting Date')
Here, index funds' cumulative return (i.e. % gain / loss) is computed, and the benchmark's cumulative return (i.e. % gain / loss) is subtracted from it. The funds' values are normalized at the start of the time period (all funds starting from value=0).
There are four comparisons with varying time scales:
start_date = fund_start_date['PNB Phil-Index Tracker Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = df_tri_comp[fund] / base
for fund in fund_names:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi']) * 100
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Cumulative Return Difference of Index Funds vs PSEi (12 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Fund Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['First Metro Equity Exchange-Traded Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = df_tri_comp[fund] / base
for fund in fund_names:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi']) * 100
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Cumulative Return Difference of Index Funds vs PSEi (6 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Fund Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['CTBC Bank - Sun Life Philippine Stock Index Feeder Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = df_tri_comp[fund] / base
for fund in fund_names:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi']) * 100
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Cumulative Return Difference of Index Funds vs PSEi (3 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Fund Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['PSEi Total Return']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
for fund in fund_names:
base = df_tri_comp[fund][df_tri_comp.index == start_date].to_numpy()[0]
df_tri_comp[fund] = df_tri_comp[fund] / base
for fund in fund_names:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi']) * 100
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.dropna(axis='columns').rolling(window=50).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Cumulative Return Difference of Index Funds vs PSEi (1 Year)')
ax.set_xlabel('Date')
ax.set_ylabel('Fund Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
Here, index funds' daily return (i.e. % gain / loss for the day) is computed, and the benchmark daily return (i.e. % gain / loss of PSEi for the day) is subtracted from it. This return difference is explored below.
There are four comparisons with varying time scales:
start_date = fund_start_date['PNB Phil-Index Tracker Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
df_tri_comp = df_tri_comp.dropna(axis='columns').pct_change().dropna() * 100
for fund in df_tri_comp.columns:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi'])
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.rolling(window=255).mean().plot(kind='line', figsize=(20,10), color=my_colors(df_tri_comp), title='Day Change Difference of Index Funds vs PSEi (12 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Daily Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['First Metro Equity Exchange-Traded Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
df_tri_comp = df_tri_comp.dropna(axis='columns').pct_change().dropna() * 100
for fund in df_tri_comp.columns:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi'])
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.rolling(window=255).mean().plot(kind='line', figsize=(20,10), ylim=(-.08,.03), color=my_colors(df_tri_comp), title='Day Change Difference of Index Funds vs PSEi (6 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Daily Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['CTBC Bank - Sun Life Philippine Stock Index Feeder Fund']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
df_tri_comp = df_tri_comp.dropna(axis='columns').pct_change().dropna() * 100
for fund in df_tri_comp.columns:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi'])
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.rolling(window=255).mean().plot(kind='line', figsize=(20,10), ylim=(-.04,.06), color=my_colors(df_tri_comp), title='Day Change Difference of Index Funds vs PSEi (3 Years)')
ax.set_xlabel('Date')
ax.set_ylabel('Daily Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
start_date = fund_start_date['PSEi Total Return']
end_date = pd.Timestamp(2020, 3, 16)
df_tri_comp = df[(df.index >= start_date) & (df.index <= end_date)].copy()
df_tri_comp = df_tri_comp.dropna(axis='columns').pct_change().dropna() * 100
for fund in df_tri_comp.columns:
if (fund is not 'PSEi'):
df_tri_comp[fund] = (df_tri_comp[fund] - df_tri_comp['PSEi'])
df_tri_comp['PSEi'] = 0
ax = df_tri_comp.rolling(window=120).mean().plot(kind='line', figsize=(20,10), ylim=(-.02,.06), color=my_colors(df_tri_comp), title='Day Change Difference of Index Funds vs PSEi (1 Year)')
ax.set_xlabel('Date')
ax.set_ylabel('Daily Percent Gain (Loss) Difference from PSEi Percent Gain (Loss)')
This is a work-in-progress section.
for i in range(len(fund_names)):
print('{}: {}'.format(i, fund_names[i]))
df_fund = df.copy()
df_fund = df_fund.iloc[::1,:]
benchmark_id_list = [-2, -1]
for benchmark_id in benchmark_id_list:
benchmark = fund_names[benchmark_id]
column_list = ['Fund Name', 'Tracking Error (STDDEV)', 'Tracking Error (R^2)']
df_TE = pd.DataFrame(columns=column_list)
for i in range(len(fund_names) - 2):
fund = fund_names[i]
fund_return = df_fund[[fund, benchmark]].pct_change().dropna().to_numpy()
r2 = r2_score(fund_return[:,1], fund_return[:,0])
fund_td = (fund_return[:,0] - fund_return[:,1])
stddev = np.std(fund_td, axis=0, ddof=0)
stddev = '{:.2f}%'.format(float(stddev*100))
stddev_x = np.std(fund_return[:,0])
TE = np.sqrt(1 - r2) * stddev_x
TE = '{:.2f}%'.format(float(TE*100))
df_TE = df_TE.append(pd.Series([fund, stddev, TE], index=column_list), ignore_index=True)
print('Index Fund Tracking Error vs {}'.format(benchmark))
display(df_TE)
print('')