CSPX IBKR vs. Infinity U.S. 500 Index Fund

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:

  1. Managing fund transfers to one's IBKR account
  2. Handling order placements on a regular basis and avoiding timing the market
  3. Figuring out a strategy for handling estate taxes in case of death

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 look at the math

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.

Code

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.

A mathematical exercise: calculating the effective expense ratio of the Infinity U.S. 500 Index Fund

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 xx. Your investment growth is 8% a year, and let's ignore inflation.

Let λ\lambda be your effective expense ratio. Every year, you invest xx, your overall investment grows at 8%, and shrinks at a rate of λ\lambda.

Let α=1λ\alpha = 1 - \lambda

Over nn years, your total investment S=αn(1.08)nx+αn1(1.08)n1x++α2(1.08)2x+α(1.08)xS = \alpha^n (1.08)^n x + \alpha^{n-1} (1.08)^{n-1} x + \dots + \alpha^2 (1.08)^2 x + \alpha (1.08) x.

Let β=1.08α\beta = 1.08\alpha, we also know that 1.08α1.08\alpha > 1.

S=βnx+βn1x++β2x+βxS = \beta^n x + \beta^{n-1} x + \dots + \beta^2 x + \beta x

For each year, we know what SS evaluates to, i.e SS is a constant. We want to find the value of β\beta.

βS=βn+1x+βnx++β3x+β2x\beta \cdot S = \beta^{n + 1} x + \beta^{n} x + \dots + \beta^3 x + \beta^2 x

(β1)S=βn+1xβx(\beta - 1) \cdot S = \beta^{n + 1} x - \beta x

S=βn+1xβxβ1=(βx)(βn1)β1S = \frac{\beta^{n + 1} x - \beta x}{\beta - 1} = \frac{(\beta x)(\beta^{n} - 1)}{\beta - 1}

Sx=β(βn1)β1\frac{S}{x} = \frac{\beta(\beta^{n} - 1)}{\beta - 1}

Solving for β\beta analytically is difficult when nn is large.

Instead, we reach for numerical methods to approximate β\beta.

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.

What about ILPs?

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.