14 Apr 2024
Someone asked me if it made sense to invest in the Infinity U.S. 500 Index Fund rather than invest in the S&P through IBKR directly.
This is not a straightforward question to answer. We can scope the question down to a pure comparison of financial outcomes, i.e. which option results in the higher returns overall? But we should keep in mind that investing in IBKR also requires additional effort in:
Depending how much you value your time and effort, the above points could sway your decision in favour of investing in a managed index fund through your financial advisor, even if it theoretically results in lower returns.
Disclaimer: I am not a financial expert, but I can do math given relevant facts and premises. This analysis is also relevant primarily to a Singaporean audience. Let me know if I missed anything!
Let's simplify the discussion by comparing investing in CSPX via IBKR vs. Infinity U.S. 500 Index Fund (which you can do yourself if you are an accredited investor or through your financial advisor).
(Why CSPX? Ireland-Domiciled ETFs have lower dividend withholding taxes.)
Infinity
CSPX via IBKR
The Frugal Student has a great analysis of CSPX.
Simplifying assumptions for calculations
These assumptions aren't accurate, but they are sufficient to show in broad strokes the behaviour of these two options over time.
At a glance, we can see that Infinity is going to lose to CSPX in terms of performance just based on the expense ratios and initial charges.
Let's spell this out, assuming a yearly investment of 12K (1K per month) over 40 years.
import pandas as pd
from tabulate import tabulate
MONTHLY_INVESTMENT = 1000
FUND_EXPENSE_RATIO_PCT = 0.61
FUND_INITIAL_CHARGE_PCT = 2
SP_GROWTH = 8
NUM_YEARS = 40
IBKR_EXPENSE_RATIO_PCT = 0.34
IBKR_FX_PCT = 0.002
fund_investment = 0
ibkr_investment = 0
years = list(range(1, NUM_YEARS + 1))
fund_investment_over_time = []
ibkr_investment_over_time = []
for year in range(NUM_YEARS):
amount_invested_that_year = 12 * (MONTHLY_INVESTMENT)
# Start of year
fund_investment += ((100 - FUND_INITIAL_CHARGE_PCT) / 100) * amount_invested_that_year
ibkr_investment += ((100 - IBKR_FX_PCT) / 100) * amount_invested_that_year
# apply expense ratio
fund_investment *= (100 - FUND_EXPENSE_RATIO_PCT) / 100
ibkr_investment *= (100 - IBKR_EXPENSE_RATIO_PCT) / 100
# apply growth
fund_investment *= (100 + SP_GROWTH) / 100
ibkr_investment *= (100 + SP_GROWTH) / 100
fund_investment_over_time.append(fund_investment)
ibkr_investment_over_time.append(ibkr_investment)
fund_investment_over_time = [round(x, 2) for x in fund_investment_over_time]
ibkr_investment_over_time = [round(x, 2) for x in ibkr_investment_over_time]
df = pd.DataFrame({'Year': years, 'Infinity': fund_investment_over_time, 'IBKR': ibkr_investment_over_time})
df = df.set_index('Year')
print(tabulate(df, headers = 'keys', tablefmt = 'psql', floatfmt=".0f"))
Output
+--------+------------+---------+
| Year | Infinity | IBKR |
|--------+------------+---------|
| 1 | 12623 | 12916 |
| 2 | 26173 | 26817 |
| 3 | 40718 | 41780 |
| 4 | 56331 | 57884 |
| 5 | 73089 | 75218 |
| 6 | 91078 | 93875 |
| 7 | 110388 | 113956 |
| 8 | 131115 | 135570 |
| 9 | 153364 | 158833 |
| 10 | 177246 | 183873 |
| 11 | 202881 | 210823 |
| 12 | 230398 | 239830 |
| 13 | 259936 | 271052 |
| 14 | 291641 | 304656 |
| 15 | 325675 | 340826 |
| 16 | 362206 | 379756 |
| 17 | 401420 | 421657 |
| 18 | 443512 | 466757 |
| 19 | 488695 | 515300 |
| 20 | 537194 | 567547 |
| 21 | 589254 | 623783 |
| 22 | 645136 | 684310 |
| 23 | 705120 | 749458 |
| 24 | 769507 | 819578 |
| 25 | 838622 | 895051 |
| 26 | 912810 | 976284 |
| 27 | 992444 | 1063718 |
| 28 | 1077925 | 1157825 |
| 29 | 1169681 | 1259115 |
| 30 | 1268173 | 1368136 |
| 31 | 1373895 | 1485479 |
| 32 | 1487379 | 1611778 |
| 33 | 1609194 | 1747718 |
| 34 | 1739951 | 1894033 |
| 35 | 1880308 | 2051517 |
| 36 | 2030968 | 2221020 |
| 37 | 2192689 | 2403462 |
| 38 | 2366282 | 2599829 |
| 39 | 2552619 | 2811185 |
| 40 | 2752635 | 3038673 |
+--------+------------+---------+
After 40 years, this results in IBKR outperforming by ~290K. Not even close basically.
The initial charge of 2% is applied specifically on the investment amount, while the expense ratio is applied on the total investment. At a glance, we know this is not a straightforward calculation as the initial charge is only applied once, while the expense ratio is applied every year. Effectively, the initial charge is amortized over the period of investment, so we expect the effective expense ratio to be a function of the number of years invested.
It would be interesting to get a sense on what the effective expense ratio for the fund is, factoring in the initial charge.
Suppose your yearly investment sum is . Your investment growth is 8% a year, and let's ignore inflation.
Let be your effective expense ratio. Every year, you invest , your overall investment grows at 8%, and shrinks at a rate of .
Let
Over years, your total investment .
Let , we also know that > 1.
For each year, we know what evaluates to, i.e is a constant. We want to find the value of .
Solving for analytically is difficult when is large.
Instead, we reach for numerical methods to approximate .
In this case, Newton's method is quite sufficient for our purposes.
Newton's method
def newton(f,Df,x0,epsilon,max_iter):
'''Approximate solution of f(x)=0 by Newton's method.
Parameters
----------
f : function
Function for which we are searching for a solution f(x)=0.
Df : function
Derivative of f(x).
x0 : number
Initial guess for a solution f(x)=0.
epsilon : number
Stopping criteria is abs(f(x)) < epsilon.
max_iter : integer
Maximum number of iterations of Newton's method.
Returns
-------
xn : number
Implement Newton's method: compute the linear approximation
of f(x) at xn and find x intercept by the formula
x = xn - f(xn)/Df(xn)
Continue until abs(f(xn)) < epsilon and return xn.
If Df(xn) == 0, return None. If the number of iterations
exceeds max_iter, then return None.
Examples
--------
>>> f = lambda x: x**2 - x - 1
>>> Df = lambda x: 2*x - 1
>>> newton(f,Df,1,1e-8,10)
Found solution after 5 iterations.
1.618033988749989
'''
xn = x0
for n in range(0,max_iter):
fxn = f(xn)
if abs(fxn) < epsilon:
print('Found solution after',n,'iterations.')
return xn
Dfxn = Df(xn)
if Dfxn == 0:
print('Zero derivative. No solution found.')
return None
xn = xn - fxn/Dfxn
print('Exceeded maximum iterations. No solution found.')
return None
Our equations:
Computing the derivative of the first equation is left as an exercise to the reader.
Code
# n: year of investment, S: overall value of investment, invested: amount invested yearly
def compute_lambda_approx(n, S, invested):
f = lambda x: (x * (x**n - 1)) / (x - 1) - (S / invested)
Df = lambda x: (n * (x**(n+1)) - (n + 1)*(x**n) + 1) / (x - 1)**2
approx_beta = newton(f, Df, 1.05, 0.0001, 1000)
lambda_ = 1 - (approx_beta / (1 + (SP_GROWTH / 100)))
return lambda_ * 100
df['Infinity Effective Expense Ratio'] = df.apply(
lambda x: compute_lambda_approx(x.name, x.Infinity, 12 * MONTHLY_INVESTMENT
), axis=1)
print(tabulate(df, headers = 'keys', tablefmt = 'psql', floatfmt=".3f"))
Output
+--------+-------------+-------------+------------------------------------+
| Year | Infinity | IBKR | Infinity Effective Expense Ratio |
|--------+-------------+-------------+------------------------------------|
| 1 | 12623.330 | 12915.680 | 2.598 |
| 2 | 26173.350 | 26817.180 | 1.923 |
| 3 | 40718.120 | 41779.760 | 1.588 |
| 4 | 56330.640 | 57884.410 | 1.384 |
| 5 | 73089.310 | 75218.280 | 1.248 |
| 6 | 91078.270 | 93875.220 | 1.151 |
| 7 | 110387.830 | 113956.210 | 1.079 |
| 8 | 131114.950 | 135569.940 | 1.022 |
| 9 | 153363.680 | 158833.400 | 0.977 |
| 10 | 177245.740 | 183872.510 | 0.940 |
| 11 | 202881.030 | 210822.810 | 0.910 |
| 12 | 230398.260 | 239830.170 | 0.884 |
| 13 | 259935.580 | 271051.600 | 0.862 |
| 14 | 291641.300 | 304656.110 | 0.843 |
| 15 | 325674.600 | 340825.580 | 0.826 |
| 16 | 362206.340 | 379755.790 | 0.812 |
| 17 | 401419.960 | 421657.470 | 0.799 |
| 18 | 443512.330 | 466757.420 | 0.787 |
| 19 | 488694.780 | 515299.750 | 0.777 |
| 20 | 537194.170 | 567547.230 | 0.767 |
| 21 | 589253.990 | 623782.650 | 0.759 |
| 22 | 645135.630 | 684310.420 | 0.751 |
| 23 | 705119.650 | 749458.140 | 0.744 |
| 24 | 769507.220 | 819578.460 | 0.738 |
| 25 | 838621.610 | 895050.920 | 0.732 |
| 26 | 912809.820 | 976284.040 | 0.726 |
| 27 | 992444.340 | 1063717.530 | 0.721 |
| 28 | 1077924.990 | 1157824.640 | 0.717 |
| 29 | 1169680.950 | 1259114.760 | 0.712 |
| 30 | 1268172.890 | 1368136.140 | 0.708 |
| 31 | 1373895.320 | 1485478.920 | 0.705 |
| 32 | 1487379.050 | 1611778.230 | 0.701 |
| 33 | 1609193.850 | 1747717.720 | 0.698 |
| 34 | 1739951.310 | 1894033.190 | 0.695 |
| 35 | 1880307.940 | 2051516.630 | 0.692 |
| 36 | 2030968.440 | 2221020.470 | 0.689 |
| 37 | 2192689.220 | 2403462.200 | 0.686 |
| 38 | 2366282.240 | 2599829.340 | 0.684 |
| 39 | 2552619.080 | 2811184.790 | 0.682 |
| 40 | 2752635.270 | 3038672.590 | 0.680 |
+--------+-------------+-------------+------------------------------------+
These numbers make sense. The effective expense ratio is necessarily greater than 0.61% since we factor in an additional initial charge. The amortization effect is clearly in play as well, as a longer investment period results in a lower effective expense ratio.
As with most of these investment vehicles, there's a big cost to pulling your investment early.
ILPs complicate this picture by offering their own set of premiums, bonuses, boosters, and charges that vary depending on investment amount and the number of years of investment. They do this while allowing you to invest in an underlying sub-fund like Infinity.
They also come with additional benefits like insurance coverage, investment protection, and medical reimbursements, which seriously muddy the waters when we're analysing if they are worthwhile.
Some options include:
Intuitively however, they are likely to offer a worse financial return in exchange for these additional benefits. Moreover, withdrawing funds from these investment vehicles is more difficult and most expect a long minimum investment period before additional perks like boosters and bonuses kick in. After all, there is no such thing as a free lunch.
That being said, doing the math for this should be interesting but I'll save that for a separate post.