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.
import matplotlib.pyplot as pyplot
import numpy as np
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")
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.
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:
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>
"""
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')
"""
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')
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
"""
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()
That looked good. But Tobias recommended that we compare the algorithms against the Russell 3000. How does that look?
"""
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")
Not too shabby. But what about sharpe ratios? Would these algos pass the minimum 0.6 for consideration for inclusion in Quantopian's fund?
"""
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' )
Cool. The top 4 algos would make the cut with Sharpe Ratios over 0.6.
What about drawdown?
"""
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:
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:
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>
'''
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")
best_am_returns = round(am_backtests['am ebitda w capex 3 per month'].cumulative_performance.returns[-1]*100,1)
best_am_returns
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
"""
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' )
Well, the same algo well out ahead of the other Acquirer's Multiples variants
Let's try to optimize the Magic Formula with the same parameters. Some of the better performers are analyzed below.
'''
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")
"""
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')
"""
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' )
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).
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:
We'll be taking you through an indepth look at the distribution of returns, sharpe ratios, drawdown, and cumulative returns.
"""
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()
"""
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()
"""
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()
"""
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)
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.
"""
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
"""
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)
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:
Sharpe Ratios:
Max Drawdown:
Calmar Ratios:
Annual Returns:
Annual Volatility:
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.