# https://www.quantopian.com/posts/risk-model-white-paper-released-and-available-for-your-reading
#
# Changes
# alpha += -vol
# 'alpha': -alpha,
# regression_length= 126
# ('risk_loading_pipeline').dropna()
from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import SimpleBeta
import quantopian.optimize as opt
from quantopian.pipeline.experimental import risk_loading_pipeline
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import Momentum, ShortTermReversal, Size, Value, Volatility
MAX_GROSS_EXPOSURE = 1.0
MAX_POSITION_SIZE = 0.015
MIN_BETA_EXPOSURE = -0.3
MAX_BETA_EXPOSURE = 0.3
NUM_LONG_POSITIONS = 200
NUM_SHORT_POSITIONS = NUM_LONG_POSITIONS
def make_pipeline():
m = QTradableStocksUS()
beta = SimpleBeta(target=sid(8554),regression_length= 126,
allowed_missing_percentage=1.0)
mom = Momentum (mask=m).zscore()
rev = ShortTermReversal(mask=m).zscore()
siz = Size (mask=m).zscore()
val = Value (mask=m).zscore()
vol = Volatility (mask=m).zscore()
alpha = mom
alpha += rev
alpha += siz
alpha += val
alpha += -vol
longs = alpha .top(NUM_LONG_POSITIONS)
shrts = alpha.bottom(NUM_SHORT_POSITIONS)
return Pipeline(
#screen = (longs | shrts) & beta.percentile_between(1, 90), # small trainwreck, must-see
screen = (longs | shrts),
columns = {
'alpha': -alpha,
'beta' : beta,
'mom' : mom,
'rev' : rev,
'siz' : siz,
'val' : val,
'vol' : vol,
}
)
def initialize(context):
context.init = True
attach_pipeline(make_pipeline(), 'long_short_equity_template')
attach_pipeline(risk_loading_pipeline(), 'risk_loading_pipeline')
# Schedule trade function
schedule_function(func=trade,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_open(minutes=60),
half_days=True)
# record portfolio variables at the end of day
schedule_function(func=recording_statements,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_close(),
half_days=True)
set_commission(commission.PerShare(cost=0, min_trade_cost=0))
set_slippage(slippage.FixedSlippage(spread=0))
def before_trading_start(context, data):
context.pipeline_data = pipeline_output('long_short_equity_template')
context.risk_loading_pipeline = pipeline_output('risk_loading_pipeline').dropna()
if 'log_pipe_done' not in context: # show pipe info once
log_pipe(context, data, context.pipeline_data, 4)
#log_pipe(context, data, context.pipeline_data, 4, filter=['alpha', 'beta', ... or what-have-you])
'''
2011-01-04 05:45 log_pipe:193 INFO Rows: 400 Columns: 7
min mean max
alpha -7.53 0.49 11.76
beta 0.07 1.23 3.24
mom -3.53 -0.15 5.40
rev -2.99 0.03 4.48
siz -5.13 -0.02 3.63
val -5.79 0.05 6.04
vol -1.75 0.40 4.70
'''
log_pipe(context, data, context.risk_loading_pipeline, 4)
'''
2011-01-04 05:45 log_pipe:193 INFO Rows: 3986 Columns: 16
min mean max
basic_materials -0.53 0.06 2.26
communication_services -0.07 0.02 1.91
consumer_cyclical -0.40 0.14 2.27
consumer_defensive -0.31 0.05 2.54
energy -0.42 0.06 1.90
financial_services -0.15 0.08 1.37
health_care -0.51 0.12 2.23
industrials -0.67 0.13 2.09
momentum -10.28 -0.02 14.62
real_estate -0.07 0.03 1.53
short_term_reversal -26.58 0.21 15.23
size -7.07 -0.90 3.95
technology -0.49 0.17 2.41
utilities -0.16 0.02 1.91
value -7.45 0.15 12.17
volatility -...
'''
def trade(context, data):
pipeline_data = context.pipeline_data
# demean and normalize
alpha = pipeline_data.alpha - pipeline_data.alpha.mean()
alpha = alpha/alpha.abs().sum()
objective = opt.MaximizeAlpha( alpha )
constraints = []
constraints.append(opt.MaxGrossExposure(MAX_GROSS_EXPOSURE))
constraints.append(opt.DollarNeutral())
constraints.append(
opt.PositionConcentration.with_equal_bounds(
min=-MAX_POSITION_SIZE,
max= MAX_POSITION_SIZE
))
beta_neutral = opt.FactorExposure(
loadings=pipeline_data[['beta']],
min_exposures={'beta':MIN_BETA_EXPOSURE},
max_exposures={'beta':MAX_BETA_EXPOSURE}
)
constraints.append(beta_neutral)
# risk_model_exposure = opt.experimental.RiskModelExposure(
# context.risk_loading_pipeline,
# version=opt.Newest,
# )
# constraints.append(risk_model_exposure)
order_optimal_portfolio(
objective=objective,
constraints=constraints,
)
def log_pipe(context, data, z, num, filter=None):
''' Log info about pipeline output or, z can be any DataFrame or Series
https://www.quantopian.com/posts/overview-of-pipeline-content-easy-to-add-to-your-backtest
'''
# Options
log_nan_only = 0 # Only log if nans are present
show_sectors = 0 # If sectors, do you want to see them or not
show_sorted_details = 1 # [num] high & low securities sorted, each column
if 'log_init_done' not in context:
log.info('${} {} to {}'.format('%.0e' % (context.portfolio.starting_cash),
get_environment('start').date(), get_environment('end').date()))
context.log_init_done = 1
if not len(z):
log.info('Empty')
return
# Series ......
context.log_pipe_done = 1 ; padmax = 6
if 'Series' in str(type(z)): # is Series, not DataFrame
nan_count = len(z[z != z])
nan_count = 'NaNs {}/{}'.format(nan_count, len(z)) if nan_count else ''
if (log_nan_only and nan_count) or not log_nan_only:
pad = max(6, len(str(z.max())))
log.info('{}{}{} Series {} len {}'.format('min' .rjust(pad+5),
'mean'.rjust(pad+5), 'max' .rjust(pad+5), z.name, len(z)))
log.info('{}{}{} {}'.format(('0.2f' % z.min()) .rjust(pad+5),
('0.2f' % z.mean()).rjust(pad+5), ('0.2f' % z.max()) .rjust(pad+5), nan_count
))
return
# DataFrame ......
content_min_max = [ ['','min','mean','max',''] ] ; content = ''
for col in z.columns:
if col == 'sector' and not show_sectors: continue
nan_count = len(z[col][z[col] != z[col]])
nan_count = 'NaNs {}/{}'.format(nan_count, len(z)) if nan_count else ''
padmax = max( padmax, 6, len(str(z[col].max())) )
content_min_max.append([col, ('%.2f' % z[col].min()), ('%.2f' % z[col].mean()), ('%.2f' % z[col] .max()), nan_count])
if log_nan_only and nan_count or not log_nan_only:
content = 'Rows: {} Columns: {}'.format(z.shape[0], z.shape[1])
if len(z.columns) == 1: content = 'Rows: {}'.format(z.shape[0])
paddings = [6 for i in range(4)]
for lst in content_min_max: # set max lengths
i = 0
for val in lst[:4]: # value in each sub-list
paddings[i] = max(paddings[i], len(str(val)))
i += 1
headr = content_min_max[0]
content += ('\n{}{}{}{}{}'.format(
headr[0] .rjust(paddings[0]),
(headr[1]).rjust(paddings[1]+5),
(headr[2]).rjust(paddings[2]+5),
(headr[3]).rjust(paddings[3]+5),
''
))
for lst in content_min_max[1:]: # populate content using max lengths
content += ('\n{}{}{}{} {}'.format(
lst[0].rjust(paddings[0]),
lst[1].rjust(paddings[1]+5),
lst[2].rjust(paddings[2]+5),
lst[3].rjust(paddings[3]+5),
lst[4],
))
log.info(content)
if not show_sorted_details: return
if len(z.columns) == 1: return # skip detail if only 1 column
if filter == None: details = z.columns
for detail in details:
if detail == 'sector': continue
hi = z[details].sort_values(by=detail, ascending=False).head(num)
lo = z[details].sort_values(by=detail, ascending=False).tail(num)
content = ''
content += ('_ _ _ {} _ _ _' .format(detail))
content += ('\n\t... {} highs\n{}'.format(detail, str(hi)))
content += ('\n\t... {} lows \n{}'.format(detail, str(lo)))
if log_nan_only and not len(lo[lo[detail] != lo[detail]]):
continue # skip if no nans
log.info(content)
def recording_statements(context, data):
record(num_positions=len(context.portfolio.positions))
record(leverage=context.account.leverage)