Last active
July 22, 2020 08:31
-
-
Save twiecki/388c74710228435936e4d2144416dce3 to your computer and use it in GitHub Desktop.
momentum algo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""This algorithm is designed for validating risk model. | |
It can be configured to be: | |
* Momentum or Mean-reversion | |
* Sector-Neutral or not | |
""" | |
from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio | |
from quantopian.pipeline import Pipeline | |
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage, AverageDollarVolume, RollingLinearRegressionOfReturns, Returns | |
from quantopian.pipeline.data.builtin import USEquityPricing | |
from quantopian.pipeline.data import morningstar | |
from quantopian.pipeline.filters.morningstar import IsPrimaryShare | |
from quantopian.pipeline.classifiers.morningstar import Sector | |
import numpy as np | |
import pandas as pd | |
from quantopian.pipeline.filters import Q1500US | |
import quantopian.experimental.optimize as opt | |
STYLE = 'momentum' | |
SECTOR_NEUTRAL = True | |
# Risk Exposures | |
if SECTOR_NEUTRAL: | |
MAX_SECTOR_EXPOSURE = 0.0010 # | |
else: | |
MAX_SECTOR_EXPOSURE = 1.0 | |
MAX_BETA_EXPOSURE = 0.0020 | |
# Constraint Parameters | |
MAX_GROSS_LEVERAGE = 1.0 | |
NUM_LONG_POSITIONS = 300 | |
NUM_SHORT_POSITIONS = 300 | |
MAX_SHORT_POSITION_SIZE = 2*1.0 / (NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) | |
MAX_LONG_POSITION_SIZE = 2*1.0 / (NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) | |
class Momentum(CustomFactor): | |
inputs = [USEquityPricing.close] | |
window_length = 252 | |
def compute(self, today, assets, out, prices): | |
out[:] = ((prices[-21] - prices[-252]) / prices[-252] - | |
(prices[-1] - prices[-21]) / prices[-21]) | |
class MeanReversion1M(CustomFactor): | |
inputs = (Returns(window_length=21),) | |
window_length = 252 | |
def compute(self, today, assets, out, monthly_rets): | |
np.divide( | |
monthly_rets[-1] - np.nanmean(monthly_rets, axis=0), | |
np.nanstd(monthly_rets, axis=0), | |
out=out, | |
) | |
def make_pipeline(): | |
if STYLE == 'momentum': | |
alpha_factor = Returns(window_length=252) - Returns(window_length=20) | |
elif STYLE == 'mean_reversion': | |
alpha_factor = MeanReversion1M() | |
# Classify all securities by sector so that we can enforce sector neutrality later | |
sector = Sector() | |
# Screen out non-desirable securities by defining our universe. | |
# Removes ADRs, OTCs, non-primary shares, LP, etc. | |
# Also sets a minimum $500MM market cap filter and $5 price filter | |
mkt_cap_filter = morningstar.valuation.market_cap.latest >= 500000000 | |
price_filter = USEquityPricing.close.latest >= 5 | |
universe = Q1500US() & price_filter & mkt_cap_filter | |
combined_rank = ( | |
alpha_factor.rank(mask=universe & Q1500US).zscore() | |
) | |
# Build Filters representing the top and bottom 150 stocks by our combined ranking system. | |
# We'll use these as our tradeable universe each day. | |
longs = combined_rank.top(NUM_LONG_POSITIONS) | |
shorts = combined_rank.bottom(NUM_SHORT_POSITIONS) | |
# The final output of our pipeline should only include | |
# the top/bottom 300 stocks by our criteria | |
long_short_screen = (longs | shorts) | |
beta = 0.66*RollingLinearRegressionOfReturns( | |
target=sid(8554), | |
returns_length=5, | |
regression_length=260, | |
mask=long_short_screen | |
).beta + 0.33*1.0 | |
# Create pipeline | |
pipe = Pipeline(columns={ | |
'longs': longs, | |
'shorts': shorts, | |
'combined_rank': combined_rank, | |
'alpha_factor': alpha_factor, | |
'sector': sector, | |
'market_beta': beta | |
}, | |
screen = long_short_screen) | |
return pipe | |
def initialize(context): | |
# Here we set our slippage and commisions. Set slippage | |
# and commission to zero to evaulate the signal-generating | |
# ability of the algorithm independent of these additional | |
# costs. | |
set_commission(commission.PerShare(cost=0.0, min_trade_cost=0)) | |
set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0)) | |
context.spy = sid(8554) | |
attach_pipeline(make_pipeline(), 'long_short_equity_template') | |
# Schedule my rebalance function | |
schedule_function(func=rebalance, | |
date_rule=date_rules.month_start(), | |
time_rule=time_rules.market_open(hours=0,minutes=30), | |
half_days=True) | |
# record my 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) | |
def before_trading_start(context, data): | |
# Call pipeline_output to get the output | |
# Note: this is a dataframe where the index is the SIDs for all | |
# securities to pass my screen and the columns are the factors | |
# added to the pipeline object above | |
context.pipeline_data = pipeline_output('long_short_equity_template') | |
def recording_statements(context, data): | |
# Plot the number of positions over time. | |
record(num_positions=len(context.portfolio.positions)) | |
#record(context.portfolio.positions) | |
# Called at the start of every month in order to rebalance | |
# the longs and shorts lists | |
def rebalance(context, data): | |
### Optimize API | |
pipeline_data = context.pipeline_data | |
### Extract from pipeline any specific risk factors you want | |
# to neutralize that you have already calculated | |
risk_factor_exposures = pd.DataFrame({ | |
'market_beta': pipeline_data.market_beta.fillna(1.0) | |
}) | |
objective = opt.MaximizeAlpha(pipeline_data.combined_rank) | |
### Define the list of constraints | |
constraints = [] | |
# Constrain our maximum gross leverage | |
constraints.append(opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE)) | |
# Require our algorithm to remain dollar neutral | |
constraints.append(opt.DollarNeutral()) | |
# Add a sector neutrality constraint using the sector | |
# classifier that we included in pipeline | |
constraints.append( | |
opt.NetPartitionExposure.with_equal_bounds( | |
labels=pipeline_data.sector, | |
min=-MAX_SECTOR_EXPOSURE, | |
max=MAX_SECTOR_EXPOSURE, | |
)) | |
# Take the risk factors that you extracted above and | |
# list your desired max/min exposures to them - | |
# Here we selection +/- 0.01 to remain near 0. | |
neutralize_risk_factors = opt.WeightedExposure( | |
loadings=risk_factor_exposures, | |
min_exposures={'market_beta':-MAX_BETA_EXPOSURE}, | |
max_exposures={'market_beta':MAX_BETA_EXPOSURE} | |
) | |
constraints.append(neutralize_risk_factors) | |
constraints.append( | |
opt.PositionConcentration.with_equal_bounds( | |
min=-MAX_SHORT_POSITION_SIZE, | |
max=MAX_LONG_POSITION_SIZE | |
)) | |
order_optimal_portfolio( | |
objective=objective, | |
constraints=constraints | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment