Run the cell below to create your tear sheet, or return to your algorithm.
bt = get_backtest('5a699a71223ce142e8128aeb')
bt.create_full_tear_sheet()
import empyrical as ep
import pyfolio as pf
import numpy as np
from matplotlib import pyplot as plt
from quantopian.pipeline import Pipeline
from quantopian.research import run_pipeline
from quantopian.pipeline.experimental import QTradableStocksUS
def get_tradable_universe(start, end):
"""
Gets the tradable universe in a format that can be compared to the positions
of a backtest.
"""
pipe = Pipeline(
columns={'qtu':QTradableStocksUS()}
)
df = run_pipeline(pipe, start, end)
df = df.unstack()
df.columns = df.columns.droplevel()
df = df.astype(float).replace(0, np.nan)
return df
def volatility_adjusted_daily_return(trailing_returns):
"""
Normalize the last daily return in `trailing_returns` by the annualized
volatility of `trailing_returns`.
"""
todays_return = trailing_returns[-1]
# Volatility is floored at 2%.
volatility = max(ep.annual_volatility(trailing_returns), 0.02)
score = (todays_return / volatility)
return score
def compute_score(returns):
"""
Compute the score of a backtest from its returns.
"""
result = []
cumulative_score = 0
count = 0
daily_scores = returns.rolling(63).apply(volatility_adjusted_daily_return)
cumulative_score = np.cumsum(daily_scores[504:])
latest_score = cumulative_score[-1]
print ''
print 'Score computed between %s and %s.' % (cumulative_score.index[0].date(), daily_scores.index[-1].date())
plt.plot(cumulative_score)
plt.title('Out-of-Sample Score Over Time')
print 'Cumulative Score: %f' % latest_score
SECTORS = [
'basic_materials', 'consumer_cyclical', 'financial_services',
'real_estate', 'consumer_defensive', 'health_care', 'utilities',
'communication_services', 'energy', 'industrials', 'technology'
]
STYLES = [
'momentum', 'size', 'value', 'short_term_reversal', 'volatility'
]
POSITION_CONCENTRATION_MAX = 0.05
LEVERAGE_MIN = 0.8
LEVERAGE_MAX = 1.1
DAILY_TURNOVER_MIN = 0.05
DAILY_TURNOVER_MAX = 0.65
NET_EXPOSURE_LIMIT_MAX = 0.1
BETA_TO_SPY_99TH_MAX = 0.3
BETA_TO_SPY_100TH_MAX = 0.4
SECTOR_EXPOSURE_MAX = 0.2
STYLE_EXPOSURE_MAX = 0.4
TRADABLE_UNIVERSE_MIN = 0.9
def check_constraints(positions, transactions, returns, risk_exposures):
sector_constraints = True
style_constraints = True
constraints_met = 0
num_constraints = 9
# Position Concentration Constraint
print 'Checking positions concentration limit...'
try:
percent_allocations = pf.pos.get_percent_alloc(positions[23:])
max_pos_concentration = pf.pos.get_top_long_short_abs(percent_allocations)[2][0]
except IndexError:
max_pos_concentration = -1
if (max_pos_concentration > 0) and (max_pos_concentration <= POSITION_CONCENTRATION_MAX):
print 'PASS: Max position concentration of %.2f%% <= %.1f%%.' % (
max_pos_concentration*100,
POSITION_CONCENTRATION_MAX*100
)
constraints_met += 1
else:
print 'FAIL: Max position concentration of %.2f%% > %.1f%%.' % (
max_pos_concentration*100,
POSITION_CONCENTRATION_MAX*100
)
# Leverage Constraint
print ''
print 'Checking leverage limits...'
leverage = pf.timeseries.gross_lev(positions[23:])
if (leverage.min() >= LEVERAGE_MIN) and (leverage.max() <= LEVERAGE_MAX):
print 'PASS: Leverage range of %.2fx-%.2fx is between %.1fx-%.1fx.' % (
leverage.min(),
leverage.max(),
LEVERAGE_MIN,
LEVERAGE_MAX
)
constraints_met += 1
else:
print 'FAIL: Leverage range of %.2fx-%.2fx is not between %.1fx-%.1fx.' % (
leverage.min(),
leverage.max(),
LEVERAGE_MIN,
LEVERAGE_MAX
)
# Turnover Constraint
print ''
print 'Checking turnover limits...'
turnover = pf.txn.get_turnover(positions, transactions)
# Compute mean rolling 63 trading day turnover.
rolling_mean_turnover = turnover.rolling(63).mean()[62:]
if (rolling_mean_turnover.min() >= DAILY_TURNOVER_MIN) \
and (rolling_mean_turnover.max() <= DAILY_TURNOVER_MAX):
print 'PASS: Mean turnover range of %.2f%%-%.2f%% is between %.1f%%-%.1f%%.' % (
rolling_mean_turnover.min()*100,
rolling_mean_turnover.max()*100,
DAILY_TURNOVER_MIN*100,
DAILY_TURNOVER_MAX*100
)
constraints_met += 1
else:
print 'FAIL: Mean turnover range of %.2f%%-%.2f%% is not between %.1f%%-%.1f%%.' % (
rolling_mean_turnover.min()*100,
rolling_mean_turnover.max()*100,
DAILY_TURNOVER_MIN*100,
DAILY_TURNOVER_MAX*100
)
# Net Exposure Constraint
print ''
print 'Checking net exposure limit...'
net_exposure = pf.pos.get_long_short_pos(positions[23:])['net exposure'].abs()
if (net_exposure.max() <= NET_EXPOSURE_LIMIT_MAX):
print 'PASS: Net exposure (absolute value) of %.2f%% <= %.1f%%.' % (
net_exposure.max()*100,
NET_EXPOSURE_LIMIT_MAX*100
)
constraints_met += 1
else:
print 'FAIL: Net exposure (absolute value) of %.2f%% on %s > %.1f%%.' % (
net_exposure.max()*100,
net_exposure.argmax().date(),
NET_EXPOSURE_LIMIT_MAX*100
)
# Beta Constraint
print ''
print 'Checking beta-to-SPY limit...'
beta = pf.timeseries.rolling_beta(returns, pf.utils.get_symbol_rets('SPY')).abs().dropna()
beta_99 = beta.quantile(0.99)
beta_100 = beta.max()
if (beta_99 > BETA_TO_SPY_99TH_MAX):
print 'FAIL: 99th percentile absolute beta of %.2f > %.1f.' % (
beta_99,
BETA_TO_SPY_99TH_MAX
)
elif (beta_100 > BETA_TO_SPY_100TH_MAX):
print 'FAIL: 100th percentile absolute beta of %.2f > %.1f.' % (
beta_100,
BETA_TO_SPY_100TH_MAX
)
else:
print 'PASS: Absolute beta with max of %.2f satisfies all constraints.' % (
beta_100,
)
constraints_met += 1
# Risk Exposures
rolling_mean_risk_exposures = risk_exposures.rolling(63, axis=0).mean()[62:]
# Sector Exposures
print ''
print 'Checking sector exposure limits...'
for sector in SECTORS:
absolute_mean_sector_exposure = rolling_mean_risk_exposures[sector].abs()
if (absolute_mean_sector_exposure.max() > SECTOR_EXPOSURE_MAX):
max_sector_exposure_day = absolute_mean_sector_exposure.idxmax()
largest_sector_exposure = rolling_mean_risk_exposures[sector].loc[max_sector_exposure_day]
print 'FAIL: Mean %s exposure of %.3f on %s is not between +/-%.2f.' % (
sector,
largest_sector_exposure,
max_sector_exposure_day.date(),
SECTOR_EXPOSURE_MAX
)
sector_constraints = False
if sector_constraints:
print 'PASS: All sector exposures were between +/-%.2f.' % SECTOR_EXPOSURE_MAX
constraints_met += 1
# Style Exposures
print ''
print 'Checking style exposure limits...'
for style in STYLES:
absolute_mean_style_exposure = rolling_mean_risk_exposures[style].abs()
if (absolute_mean_style_exposure.max() > STYLE_EXPOSURE_MAX):
max_style_exposure_day = absolute_mean_style_exposure.idxmax()
largest_style_exposure = rolling_mean_risk_exposures[style].loc[max_style_exposure_day]
print 'FAIL: Mean %s exposure of %.3f on %s is not between +/-%.2f.' % (
style,
largest_style_exposure,
max_style_exposure_day.date(),
STYLE_EXPOSURE_MAX
)
style_constraints = False
if style_constraints:
print 'PASS: All style exposures were between +/-%.2f.' % STYLE_EXPOSURE_MAX
constraints_met += 1
# Tradable Universe
print ''
print 'Checking investment in tradable universe...'
positions_wo_cash = positions.drop('cash', axis=1)
positions_wo_cash = positions_wo_cash.abs()
total_investment = positions_wo_cash.fillna(0).sum(axis=1)
daily_qtu_investment = universe.multiply(positions_wo_cash).fillna(0).sum(axis=1)
percent_in_qtu = daily_qtu_investment / total_investment
min_percent_in_qtu = percent_in_qtu.min()
if min_percent_in_qtu >= TRADABLE_UNIVERSE_MIN:
print 'PASS: Investment in QTradableStocksUS is >= %.1f%% at all times.' % (
TRADABLE_UNIVERSE_MIN*100
)
constraints_met += 1
else:
min_percent_in_qtu_date = percent_in_qtu.argmin()
print 'FAIL: Investment in QTradableStocksUS of %.2f%% on %s is < %.1f%%.' % (
min_percent_in_qtu*100,
min_percent_in_qtu_date.date(),
TRADABLE_UNIVERSE_MIN*100
)
# Total Returns Constraint
print ''
print 'Checking that algorithm has positive returns...'
cumulative_returns = ep.cum_returns_final(returns)
if (cumulative_returns > 0):
print 'PASS: Cumulative returns of %.2f is positive.' % (
cumulative_returns
)
constraints_met += 1
else:
print 'FAIL: Cumulative returns of %.2f is negative.' % (
cumulative_returns
)
print ''
print 'Results:'
if constraints_met == num_constraints:
print 'All constraints met!'
else:
print '%d/%d tests passed.' % (constraints_met, num_constraints)
def evaluate_backtest(positions, transactions, returns, risk_exposures):
if len(positions.index) > 504:
check_constraints(positions, transactions, returns, risk_exposures)
score = compute_score(returns[start:end])
return score
else:
print 'ERROR: Backtest must be longer than 2 years to be evaluated.'
positions = bt.pyfolio_positions
transactions = bt.pyfolio_transactions
returns = bt.daily_performance.returns
factor_exposures = bt.factor_exposures
start = positions.index[0]
end = positions.index[-1]
universe = get_tradable_universe(start, end)
universe.columns = universe.columns.map(lambda x: '%s-%s' % (x.symbol, x.sid))
evaluate_backtest(positions, transactions, returns, factor_exposures)