Notebook

Step 1: Fail

Our initial attempt at duplicating the performance of the two algorithms was definitely bumpy.

We based our algos' design on the wikipedia entry for the Magic Formula:

1. Establish a minimum market capitalization (usually greater than $50 million).
2. Exclude utility and financial stocks.
3. Exclude foreign companies (American Depositary Receipts).
4. Determine company's earnings yield = EBIT / enterprise value.
5. Determine company's return on capital = EBIT / (net fixed assets + working capital).
6. Rank all companies above chosen market capitalization by highest earnings yield and highest return on capital (ranked as percentages).
7. Invest in 20–30 highest ranked companies, accumulating 2–3 positions per month over a 12-month period.
8. Re-balance portfolio once per year, selling losers one week before the year-mark and winners one week after the year mark.
9. Continue over a long-term (5–10+ year) period.

For the Acquierer's multiple we replaced the ranking algos of #4 and #5 with a single calculation: EV/(EBIT - Capex).

In our initial implementation, we buy 5 stocks a month until we have 30 stocks. We sell each stock after about a year as specified in #8 above and purchase new stocks using the same ranking logic.

In [3]:
import matplotlib.pyplot as pyplot
import numpy as np
In [4]:
am_first_draft = get_backtest('54d52277b211f64fbe08dd42')
am_first_draft.cumulative_performance.ending_portfolio_value.plot(label="Acquirer's Multiple (1st Draft)")
mf_first_draft = get_backtest('54ce9a62fa5b3246521b5df8')
mf_first_draft.cumulative_performance.ending_portfolio_value.plot(label="Magic Formula (1st Draft)")
pyplot.legend(loc='best')
pyplot.ylabel("Portfolio Value in $")
pyplot.xlabel("Time")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.title("First Attempt")
100% Time: 0:00:57|###########################################################|
100% Time: 0:00:58|###########################################################|
Out[4]:
<matplotlib.text.Text at 0x7f8c799ee950>

Our reaction? Oh crap.

We're workign with Tobias Carlisle to duplicate his findings . . . and he's kind of a big deal. And he's spending time with us . . . and our implementation of his formula is terrible. Not only does it underperform the Magic Formula, it underperforms the S&P 500 benchmark over the 2002 - 2015 timeframe. This is going to be awkward. Either for us or for him. Or for both of us. But let's be honest. Probably for us.

But we dug in and persevered a bit and found lots of ways to improve.

Step 2: Iteratate and Persevere

First, we found a simple sorting bug in our ranking logic.

Also, we found that Capex is stored in the Morningstar database as a negative number. So our calculations like EV/(EBIT - Capex) should be EV/(EBIT + Capex).

But the biggest lesson we learned was that when we implemented the logic, we needed to use trailing 12 month versions of metrics like EBIT, and not the most recent quarterly values (as provided by fundamentals).

We set out to refactor the algo, addressing these problems as we went.

And for this iteration, we tested a variety of options, as suggested by Tobias:

  • Using EBIT and EBITDA for both strategies
  • Eliminating the use of Capex in the Acquirer's Multiple (i.e. simplifying it even further)

Note some important guidance from Tobias I got: "You'd never use EBIT-CapEx because depreciation and amortization (a substitute for CapEx) is already backed out of EBITDA to arrive at EBIT"

So now we have 5 different options to test, with our improved algorithm:

<pre> | Algo Labels | Ratios used for ranking | |----------------------------|-------------------------| | am_ebitda_with_capex | EV/(EBITDA - Capex) | | am_ebitda_without_capex | EV/(EBITDA) | | am_ebit_without_capex | EV/(EBIT) | | mf_ebitda | EV/(EBITDA) and ROIC | | mf_ebit | EV/(EBIT) and ROIC | </pre>

In [5]:
"""
Next iteration, trying with and without Capex and replacing EBIT with EBITDA
"""

backtest_names = {
'am_ebitda_with_capex' : '54e503691bcf550f0e7857aa',
'am_ebitda_without_capex' : '54e5037efd07e90f14b7e6de',

'am_ebit_without_capex' : '54e503b419f9f20f008a7266',

'mf_ebitda' : '54e503e94d32f20f1246aaf6',
'mf_ebit' : '54e503fe9011230f13328ebd'
}

backtests = {}
for b in backtest_names:
    backtests[b] = get_backtest(backtest_names[b])
    backtests[b].cumulative_performance.ending_portfolio_value.plot(label=b)
pyplot.legend(loc='best')
100% Time: 0:00:37|###########################################################|
100% Time: 0:00:39|###########################################################|
100% Time: 0:00:38|###########################################################|
100% Time: 0:00:36|###########################################################|
100% Time: 0:00:42|###########################################################|
Out[5]:
<matplotlib.legend.Legend at 0x7f8c73f5acd0>
In [6]:
"""
Let's to a cleaner comparison of the ending total returns
"""
sharpe_ratios = {}

#: Here, we're taking the average daily return and plotting that
for b in backtest_names:
    sharpe_ratios[b] = round(backtests[b].cumulative_performance.returns[-1]*100,1)
    
#: Some label creations for the horizontal bar graphs
labels = sorted(sharpe_ratios.keys(), key=lambda x: sharpe_ratios[x])
y_pos = np.arange(len(labels))
sharpes = [sharpe_ratios[s] for s in labels]

pyplot.barh(y_pos, sharpes, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("Final Portfolio Returns: 1/2002 - 2/2015")
pyplot.grid(b=None, which=u'major', axis=u'both')

for idx, pct in enumerate(sharpes):
        pyplot.annotate(" %0.1f%%" % pct, xy=(pct , idx), va='center')

Now we're getting somewhere

Looks like the three of the variants do pretty well: the Magic Formula using EBITDA in its calculations, the Acquirer's Multple using EBITDA and Capex, and the Acquirer's Multiple using EBIT but no Capex.

Let's do some quick comparisons of the best Acquirer's Multiple algo with a benchmark or two

In [7]:
"""
Comparing to SPY benchmark backtest
"""

single_bt = get_backtest('54e503691bcf550f0e7857aa')
single_bt.cumulative_performance.ending_portfolio_value.plot(label="Acquirer's multiple with CapEx")
spy_benchmark = get_backtest('54e695d6de7c210f0ba5889d')
spy_benchmark.cumulative_performance.ending_portfolio_value.plot(label="SPY Benchmark")
pyplot.ylabel("Portoflio Value in $")
pyplot.legend(loc='best')
pyplot.ylabel("Portfolio Value in $")
pyplot.xlabel("Time")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.title("Acquirer's Multiple")
#sns.despine()
100% Time: 0:00:39|###########################################################|
100% Time: 0:00:10|###########################################################|
Out[7]:
<matplotlib.text.Text at 0x7f8c70152b90>

What about using the Russell 3000 as a benchmark?

That looked good. But Tobias recommended that we compare the algorithms against the Russell 3000. How does that look?

In [8]:
"""
Comparing against IWV (ETF for the Russell 3000)
"""

single_bt = get_backtest('54e503691bcf550f0e7857aa')
single_bt.cumulative_performance.ending_portfolio_value.plot(label="Acquirer's multiple with CapEx")
iwv_benchmark = get_backtest('54e69915febcf70f039812b3')
iwv_benchmark.cumulative_performance.ending_portfolio_value.plot(label="IWV Benchmark")
pyplot.ylabel("Portoflio Value in $")
pyplot.legend(loc='best')
pyplot.ylabel("Portfolio Value in $")
pyplot.xlabel("Time")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.title("Acquirer's Multiple")
100% Time: 0:00:38|###########################################################|
100% Time: 0:00:10|###########################################################|
Out[8]:
<matplotlib.text.Text at 0x7f8c78c28150>

What about some other metrics?

Not too shabby. But what about sharpe ratios? Would these algos pass the minimum 0.6 for consideration for inclusion in Quantopian's fund?

In [9]:
"""
Analzying Sharpe Ratios
"""
sharpe_ratios = {}

#: Here, we're taking the last sharpe ratio (which is an annualized total) and plotting that
for b in backtest_names:
    sharpe_ratios[b] = backtests[b].risk.sharpe[-1]

#: Some label creations for the horizontal bar graphs
labels = sorted(sharpe_ratios.keys(), key=lambda x: sharpe_ratios[x])
y_pos = np.arange(len(labels))
sharpes = [sharpe_ratios[s] for s in labels]

pyplot.barh(y_pos, sharpes, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("Sharpe Ratios")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.axvline(x=.6, linewidth=1, color='g' )
Out[9]:
<matplotlib.lines.Line2D at 0x7f8c7968d790>

Making the cut

Cool. The top 4 algos would make the cut with Sharpe Ratios over 0.6.

What about drawdown?

In [10]:
"""
Max Drawdown
"""
import numpy as np
max_drawdowns = {}

#: Here, we're taking the average daily return and plotting that
for b in backtest_names:
    max_drawdowns[b] = backtests[b].risk.max_drawdown.iloc[-1]
    
#: Some label creations for the horizontal bar graphs
labels = sorted(max_drawdowns.keys(), key=lambda x: max_drawdowns[x])
y_pos = np.arange(len(labels))
mds = [max_drawdowns[s] for s in labels]

pyplot.barh(y_pos, mds, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("Max Drawdown")
pyplot.grid(b=None, which=u'major', axis=u'both')

Interesting.

Magic Formula generally outperforms the Acquirer's Multiple when it comes to drawdown, especially the variant using EBIT in it's calculations.

Analyzing these results so far, it looks like 2 algos are doing the best:

  • Acquirer's Multiple using EBITDA and Capex
  • Magic Formula using EBITDA

Step 3: Can we optimize the algorithms?

OK, now that we'd gotten to a point where the performance of the algorithms was terrible and I wasn't going to be embarrassing myself with Tobias.

Can we do even better?

We've got a bunch of variables we can experiment with:

  • Total number of securities held
  • Total number of securities purchased per month
  • Duration of holding period for positions

We tried lots and lots of different options. We'll show some of the better variants.

Here's a quick description of each of the algorithms we'll plot out: <pre> | Algo Labels | Ratios used for ranking | Port. Size | Stocks bought/month | |-----------------------------------|-------------------------|------------|---------------------| | am_ebitda_with_capex | EV/(EBITDA - Capex) | 30 | 5 | | am ebditda wo capex 7 a month | EV/EBITDA | 30 | 7 | | am ebitda wo capex 20 positions | EV/(EBITDA - Capex) | 20 | 5 | | am ebitda w capex 3 per month | EV/EBIT | 30 | 3 | | mf_ebitda | EV/EBITDA and ROIC | 30 | 5 | | mf ebit with 20 positions | EV/EBIT and ROIC | 20 | 5 | | mf ebit 3 per month | EV/EBIT and ROIC | 30 | 3 | | mf ebitda 3 per month | EV/EBITDA and ROIC | 30 | 3 | </pre>

In [11]:
'''
Here's the best Acquirer's Multiple with other strong performing variants from
the optimization exercise
'''

am_names = {
'am_ebitda_with_capex' : '54e503691bcf550f0e7857aa',
'am ebitda wo capex 7 a month' : '54e7593c6092390f056d0347',
'am ebitda wo capex 20 positions' : '54e75911d5f28d0f1a4d68b4',
'am ebitda w capex 3 per month' : '54ed2d793e03920f1fbd7668'
}

am_backtests = {}
for b in am_names:
    am_backtests[b] = get_backtest(am_names[b])
    am_backtests[b].cumulative_performance.ending_portfolio_value.plot(label=b)
 
pyplot.ylabel("Portoflio Value in $")
pyplot.legend(loc='best')
pyplot.ylabel("Portfolio Value in $")
pyplot.xlabel("Time")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.title("Acquirer's Multiple Top Performers")
100% Time: 0:00:39|###########################################################|
100% Time: 0:00:37|###########################################################|
100% Time: 0:00:39|###########################################################|
100% Time: 0:00:29|###########################################################|
Out[11]:
<matplotlib.text.Text at 0x7f8c7210f190>
In [32]:
best_am_returns = round(am_backtests['am ebitda w capex 3 per month'].cumulative_performance.returns[-1]*100,1)
best_am_returns
Out[32]:
331.9

A new winner

From this pool of Acquirer's Multiple variants, it looks like we've got a new top performer, returns wise. Using EBITDA and rebalancing 3 stocks at a time, seems to get the best results -- 331.9% over the course of the full backtest.

This makes sense as it gives more frequent opportunities to pick up undervalued stocks. When rebalancing 5 stocks at a time, we're only rebalancing 6 months out of every 12.

Let's check out the Sharpe Ratios

In [28]:
"""
OK, Let's compare Sharpe ratios for the top performers
"""
sharpe_ratios = {}

#: Here, we're taking the average daily return and plotting that
for b in am_names:
    sharpe_ratios[b] = am_backtests[b].risk.sharpe[-1]
    
#: Some label creations for the horizontal bar graphs
labels = sorted(sharpe_ratios.keys(), key=lambda x: sharpe_ratios[x])
y_pos = np.arange(len(labels))
sharpes = [sharpe_ratios[s] for s in labels]

pyplot.barh(y_pos, sharpes, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("Sharpe Ratios of Acquirer's Multiple Variants")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.axvline(x=.6, linewidth=1, color='g' )
Out[28]:
<matplotlib.lines.Line2D at 0x7f8c71d88710>

Clear winner again

Well, the same algo well out ahead of the other Acquirer's Multiples variants

Magic Formula Optimization

Let's try to optimize the Magic Formula with the same parameters. Some of the better performers are analyzed below.

In [12]:
'''
Here's the best Magic Formula with the same ideas from the cell before
'''

mf_names = {
'mf_ebitda' : '54e503e94d32f20f1246aaf6',
'mf ebit with 20 positions' : '54e76d906092390f056d5aa7',
'mf ebit 3 per month' : '54ee712af91fed0f0ee7b570',
'mf ebitda 3 per month' : '54ee86fa0e5f420f0ed77f22'
}

mf_backtests = {}
for b in mf_names:
    mf_backtests[b] = get_backtest(mf_names[b])
    mf_backtests[b].cumulative_performance.ending_portfolio_value.plot(label=b)
pyplot.ylabel("Portoflio Value in $")
pyplot.legend(loc='best')
pyplot.ylabel("Portfolio Value in $")
pyplot.xlabel("Time")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.title("Magic Formula Variants")
100% Time: 0:00:28|###########################################################|
100% Time: 0:00:36|###########################################################|
100% Time: 0:00:36|###########################################################|
100% Time: 0:00:42|###########################################################|
Out[12]:
<matplotlib.text.Text at 0x7f8c79806ad0>
In [13]:
"""
Let's get a cleaner look at the overall returns for the Magic Formula variants

But it looks like we haven't improved on our first iteration for the Magic Formula in the same way
"""

mf_returns = {}

#: Here, we're taking the average daily return and plotting that
for b in mf_names:
    mf_returns[b] = round(mf_backtests[b].cumulative_performance.returns[-1]*100,1)
    
#: Some label creations for the horizontal bar graphs
labels = sorted(mf_returns.keys(), key=lambda x: mf_returns[x])
y_pos = np.arange(len(labels))
returns = [mf_returns[s] for s in labels]

pyplot.barh(y_pos, returns, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("MF Final Portfolio Returns: 1/2002 - 2/2015")
pyplot.grid(b=None, which=u'major', axis=u'both')

for idx, pct in enumerate(returns):
        pyplot.annotate(" %0.1f%%" % pct, xy=(pct , idx), va='center')
In [36]:
"""
OK, Let's compare Sharpe ratios for these
"""
sharpe_ratios = {}

#: Here, we're taking the average daily return and plotting that
for b in mf_names:
    sharpe_ratios[b] = mf_backtests[b].risk.sharpe[-1]
    
#: Some label creations for the horizontal bar graphs
labels = sorted(sharpe_ratios.keys(), key=lambda x: sharpe_ratios[x])
y_pos = np.arange(len(labels))
sharpes = [sharpe_ratios[s] for s in labels]

pyplot.barh(y_pos, sharpes, align='center', alpha=0.8)
pyplot.yticks(y_pos, labels)
pyplot.title("Sharpe Ratios of Magic Formula Variants")
pyplot.grid(b=None, which=u'major', axis=u'both')
pyplot.axvline(x=.6, linewidth=1, color='g' )
Out[36]:
<matplotlib.lines.Line2D at 0x7f8c73072450>

Not much improvement

Looking at these new variations, our original Magic Formula winner still reigns. 5 stocks purchased per month, building up to a 30 stock portfolio, rebalancing at around the year mark seems to work best. Where we're using EBITDA (as opposed to EBIT).

Comparing the best Acquirer's Multiple and Magic Formula variants

Now that we have a good idea of which variant seems to perform the best (in terms of returns), we're going to compare the top performers in both categories. So that being said, the winners are:

  • Acquirer's Multiple using EBITDA and subtracting out the Capex; trading on 3 securities per month
  • Magic Formula using EBITDA; trading 5 securities per month

We'll be taking you through an indepth look at the distribution of returns, sharpe ratios, drawdown, and cumulative returns.

In [21]:
"""
Looking at the distribution of returns for Aqcuirer's Multiple
"""

import pandas as pd
import seaborn as sns

top_two_backtests = {
    'am ebitda w capex 3 per month': am_backtests['am ebitda w capex 3 per month'],
    'mf_ebitda': mf_backtests['mf_ebitda']
}

am_title = 'am ebitda w capex 3 per month'
am_returns = top_two_backtests[am_title].cumulative_performance.ending_portfolio_value.pct_change().dropna()
sns.distplot(am_returns, label=am_title)

pyplot.xlim([-.10,.10]) 
pyplot.ylim([0,140]) 
pyplot.legend()
Out[21]:
<matplotlib.legend.Legend at 0x7f8c78db7c50>
In [20]:
"""
Looking at the distribution of returns for Magic Formula
"""

mf_title = 'mf_ebitda'
mf_returns = top_two_backtests[mf_title].cumulative_performance.ending_portfolio_value.pct_change().dropna()
sns.distplot(mf_returns, label=mf_title)

pyplot.xlim([-.10,.10]) 
pyplot.legend()
Out[20]:
<matplotlib.legend.Legend at 0x7f8c72136a50>
In [46]:
"""
Let's compare the two, overlayed. Bars are tough to see, so just use the overlay lines and zoom
in a bit
"""

mf_title = 'mf_ebitda'
mf_returns = top_two_backtests[mf_title].cumulative_performance.ending_portfolio_value.pct_change().dropna()
sns.kdeplot(mf_returns, label=mf_title)
am_title = 'am ebitda w capex 3 per month'
am_returns = top_two_backtests[am_title].cumulative_performance.ending_portfolio_value.pct_change().dropna()
sns.kdeplot(am_returns, label=am_title)

pyplot.xlim([-.05,.05])
pyplot.title("Distribution of Daily Returns of Magic Formula vs. Acquirer's Multiple")
pyplot.legend()
Out[46]:
<matplotlib.legend.Legend at 0x7f8c720a2e10>
In [17]:
"""
Look at the distribution of returns side by side
"""

returns = {'am_returns': am_returns, 'mf_returns': mf_returns}
df = pd.DataFrame(returns)
sns.violinplot(df)
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f8c73b1efd0>

At first glance, it appears that the volatilty of the Magic Formula variation seems to be slightly higher than the Acquirer's multiple. But it's still hard to say at this point. What we need to do is actually dig in deeper to look at a number of different metrics in order to find what we're looking for. To do that we'll examine: Cumulative Returns, Sharpe Ratios, Max Drawdown, Calmar Ratios, Annual Returns, and Annual Volatility.

In [43]:
"""
Write a number of different helper functions that will help us plot and analyze our backtests
"""

def create_dict_with_backtest(backtest_dict, calc_type):
    """
    Returns a dictionary that contains the specified metric from the backtest
    """
    plot_dict = {}
    for title, backtest in backtest_dict.iteritems():
        if calc_type == 'drawdowns':
            calc = backtest.risk.max_drawdown.iloc[-1]
        elif calc_type == 'total_returns': 
            calc = backtest.cumulative_performance.returns[-1]*100
        elif calc_type == 'sharpe_ratios':
            calc = backtest.risk.sharpe[-1]
        elif calc_type == 'annual_returns':
            calc = annual_return(backtest.cumulative_performance.ending_portfolio_value)
        elif calc_type == 'annual_volatility':
            calc = annual_volatility(backtest.cumulative_performance.ending_portfolio_value)
        elif calc_type == 'calmar_ratios':
            calc = calmar_ratio(backtest.cumulative_performance.ending_portfolio_value)
        elif calc_type == 'stability_of_ts':
            calc = stability_of_time_series(backtest.cumulative_performance.ending_portfolio_value)
        plot_dict[title] = calc
    return plot_dict

def create_labels_and_y_pos(plot_dict):
    """
    Returns labels and y_pos for the plot
    """
    labels = sorted(plot_dict.keys(), key=lambda x: plot_dict[x])
    y_pos = np.arange(len(labels))
    return labels, y_pos

def create_plot(plot_dict, y_pos, labels, color, xlabel, title, subplot_pos, subplot_len):
    """
    Creates a bar plot
    """
    ax = fig.add_subplot(subplot_len, 1, subplot_pos)
    ax.grid(b=False)
    ax.barh(y_pos, plot_dict, align='center', alpha=0.6, color=color)
    pyplot.yticks(y_pos, labels)
    #pyplot.xlabel(xlabel)
    pyplot.title(title)
    
    for idx, num in enumerate(plot_dict):
        pyplot.annotate("%0.2f" % num, xy=(num, idx), va='center')

def annual_return(ts, input_is_nav=True, style='calendar'):
    # if style == 'compound' then return will be calculated in geometric terms: (1+mean(all_daily_returns))^252 - 1
    # if style == 'calendar' then return will be calculated as ((last_value - start_value)/start_value)/num_of_years
    # if style == 'arithmetic' then return is simply mean(all_daily_returns)*252
    if ts.size < 1:
        return np.nan
    
    if input_is_nav:
        temp_returns = ts.pct_change().dropna()
        if style == 'calendar':
            num_years = len(temp_returns) / 252
            start_value = ts[0]
            end_value = ts[-1]
            return ((end_value - start_value)/start_value) / num_years
        if style == 'compound':
            return pow( (1 + temp_returns.mean()), 252 ) - 1
        else:
            return temp_returns.mean() * 252
    else:
        if style == 'calendar':
            num_years = len(ts) / 252
            temp_nav = cum_returns(ts, with_starting_value=100)
            start_value = temp_nav[0]
            end_value = temp_nav[-1]
            return ((end_value - start_value)/start_value) / num_years
        if style == 'compound':
            return pow( (1 + ts.mean()), 252 ) - 1
        else:
            return ts.mean() * 252


def annual_volatility(ts, input_is_nav=True):
    if ts.size < 2:
        return np.nan
    if input_is_nav:
        temp_returns = ts.pct_change().dropna()
        return temp_returns.std() * np.sqrt(252)
    else:
        return ts.std() * np.sqrt(252)


def calmar_ratio(ts, input_is_nav=True):
    temp_max_dd = max_drawdown(ts=ts)
    if temp_max_dd < 0:
        if input_is_nav:
            temp = annual_return(ts=ts, input_is_nav=input_is_nav) / abs(max_drawdown(ts=ts))
        else:
            temp_nav = cum_returns(ts,with_starting_value=100)
            temp = annual_return(ts=ts, input_is_nav=input_is_nav) / abs(max_drawdown(ts=temp_nav))
    else:
        return np.nan
    
    if np.isinf(temp):
        return np.nan
    else:
        return temp

def stability_of_time_series(ts, log_value=True ):
    if ts.size < 2:
        return np.nan
    
    ts_len = ts.size
    X = range(0, ts_len)
    X = sm.add_constant(X)
    if log_value:
        temp_values = np.log10(ts.values)
    else:
        temp_values = ts.values
    model = sm.OLS(temp_values, X).fit()
    
    return model.rsquared

def normalize(df, with_starting_value=1):
    if with_starting_value > 1:
        return with_starting_value * ( df / df.iloc[0] )
    else:
        return df / df.iloc[0]

def cum_returns(df, with_starting_value=None):
    if with_starting_value is None:
        return (1 + df).cumprod() - 1
    else:
        return (1 + df).cumprod() * with_starting_value
    
def max_drawdown(ts):
    MDD = 0
    DD = 0
    peak = -99999
    for value in ts:
        if (value > peak):
            peak = value
        else:
            DD = (peak - value) / peak
        if (DD > MDD):
            MDD = DD
    return -1*MDD
In [44]:
"""
Looking at Sharpe, Drawdown, and Returns
"""

drawdowns = create_dict_with_backtest(top_two_backtests, 'drawdowns')
drawdown_labels, drawdown_y_pos = create_labels_and_y_pos(drawdowns)
drawdowns = [drawdowns[s] for s in drawdown_labels]

total_returns = create_dict_with_backtest(top_two_backtests, 'total_returns')
return_labels, return_y_pos = create_labels_and_y_pos(total_returns)
total_returns = [total_returns[s] for s in return_labels]
    
sharpe_ratios = create_dict_with_backtest(top_two_backtests, 'sharpe_ratios')
labels, y_pos = create_labels_and_y_pos(sharpe_ratios)
sharpe_ratios = [sharpe_ratios[s] for s in labels]

calmar_ratios = create_dict_with_backtest(top_two_backtests, 'calmar_ratios')
calmar_labels, calmar_y_pos = create_labels_and_y_pos(calmar_ratios)
calmar_ratios = [calmar_ratios[s] for s in calmar_labels]

annual_returns = create_dict_with_backtest(top_two_backtests, 'annual_returns')
annual_labels, annual_y_pos = create_labels_and_y_pos(annual_returns)
annual_returns = [annual_returns[s] for s in annual_labels]

annual_volatility = create_dict_with_backtest(top_two_backtests, 'annual_volatility')
vol_labels, vol_y_pos = create_labels_and_y_pos(annual_volatility)
annual_volatility = [annual_volatility[s] for s in vol_labels]

#: Creating the subplots
fig = pyplot.figure()
plot_num = 6
col = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]
create_plot(total_returns, return_y_pos, return_labels, col[0], "% Return", "Cumulative Returns", 1, plot_num)
create_plot(sharpe_ratios, y_pos, labels, col[1], "Sharpe", "Sharpe Ratios", 2, plot_num)
create_plot(drawdowns, drawdown_y_pos, drawdown_labels, col[2], "% Drawdown", "Max Drawdown", 3, plot_num)
create_plot(calmar_ratios, calmar_y_pos, calmar_labels, col[3], "Calmar", "Calmar Ratios", 4, plot_num)
create_plot(annual_returns, annual_y_pos, annual_labels, col[4], "% Return", "Annual Returns", 5, plot_num)
create_plot(annual_volatility, vol_y_pos, vol_labels, col[5], "% Volatility", "Annual Volatility", 6, plot_num)
fig.subplots_adjust(wspace=.4, hspace=2)
sns.despine(left=True)

Analyzing the Results

Taking a look at the Cumulative Returns, Sharpe Ratios, Max Drawdown, Calmar Ratios, Annual Returns, and Annual Volatility we see that the Acquirer's Multiple beats the Magic Formula in almost every category except drawdown and annual volatility. However, given the Sharpe ratio and Calmar ratio, it seems that the volatility and drawdown is for good measure. So without further ado, here are the results in print form:

Cumulative Returns:

  • Acquirer's Multiple = 331.87%
  • Magic Formula = 253.67%

Sharpe Ratios:

  • Acquirer's Multiple = 1.40
  • Magic Formula = 1.13

Max Drawdown:

  • Acquirer's Multiple = 44%
  • Magic Formula = 43%

Calmar Ratios:

  • Acquirer's Multiple = .58
  • Magic Formula = .45

Annual Returns:

  • Acquirer's Multiple = 26%
  • Magic Formula = 20%

Annual Volatility:

  • Acquirer's Multiple = 17%
  • Magic Formula = 15%

In short, Magic Formula has slightly better performance with defensive metrics like volatility and drawdawn, but the superior returns that the Acquirer's Multiple provides proves to be worth it, as encapsulated in metrics like Sharpe Ratio and Calmar Ratio.

Simpler, it seems, really is better in this case.