# MomAtFairPrice_EquityLongShort_TonyM_001
# 11 Dec 2019.
# As per version _TonyM_000, but modify as follows for Quantopian contest entry:
# - Increase initial trading capital from $100k to $ 10MM
# - Increase number of trading positions (Long & Short) from 10 to 100.
# - Reduce maximum gross exposure from 0.95 to 0.90
# - Re-set rebalance to Weekly.
# - Reverse sign on PEG zscore component in "Combined"
# MomAtFairPrice_BearStockReversal_TonyM_000
# 11 Dec 2019.
# This is a copy of " Momentum at a fair price + reversal in bear markets"
# Original algo by Marcos Wernicke
# Backtest from 2014-07-01 to 2018-11-08 with $100,000 initial capital
# But now convert to "Equity Long-Short" type algo for Quantopian,
# by considering Bear STOCK reversal, rather than Bear MARKET reversal.
# ---------------------------------------------------------------
# Import Quantopian Algorithm API functions
import quantopian.algorithm as algo
#from quantopian.pipeline import Pipeline
from quantopian.algorithm import (
attach_pipeline,
pipeline_output,
order_optimal_portfolio
)
# Import Pipeline class and datasets
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
#from quantopian.pipeline.factors import AverageDollarVolume
import quantopian.optimize as opt
# Import other built-in factors. # Not used here
"""
from quantopian.pipeline.factors import (
SimpleMovingAverage,
RollingLinearRegressionOfReturns,
SimpleBeta,
Returns
)
"""
from quantopian.pipeline.filters import QTradableStocksUS #,Q500US,Q1500US
from quantopian.pipeline.experimental import risk_loading_pipeline
from quantopian.pipeline.factors import Returns, CustomFactor, Latest
import numpy as np
import pandas as pd
# Morningstar fundamentals data (if required later)
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import Fundamentals as income_statement
from quantopian.pipeline.data import Fundamentals as balance_sheet
from quantopian.pipeline.data import Fundamentals as operation_ratios
from quantopian.pipeline.data import Fundamentals as valuation_ratios
from quantopian.pipeline.classifiers.fundamentals import Sector
#Classify securities by sector to enforce sector neutrality later
sector = Sector()
#--------------------------------------------------------
# Define constraints that target portfolio must satisfy:
#-------------------
MAX_GROSS_LEVERAGE = 1.00
MAX_GROSS_EXPOSURE = 0.90 # NOMINAL leverage = 1.00, constraint 0.8x to 1.1x
MAX_BETA_EXPOSURE = 0.05 # Low beta-to-SPY
MAX_SECTOR_EXPOSURE = 0.05 # Low sector exposure
#Dollar Neutral .05
#Position Concentration .10
# Set the Number of positions used
# --------------------------------
# 10 as per Marcos' original algo. Now increase to 100
NUM_LONG_POSITIONS = 100
NUM_SHORT_POSITIONS = 100
# Modified the following 2 lines to ensure that the number of positions will actually be as specified above,
# irrespective of nominal leverage setting.
NUM_LONG_POSITIONS = int(NUM_LONG_POSITIONS / MAX_GROSS_EXPOSURE)
NUM_SHORT_POSITIONS = int(NUM_SHORT_POSITIONS / MAX_GROSS_EXPOSURE)
# Define maximum position size that can be held for any given stock.
# The optimizer needs some leeway in order to operate. If maximum is too small, the optimizer may be overly-constrained.
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)
#=================================================================
# Define own factors here:
class momentum(CustomFactor): #momentum definition
inputs = [USEquityPricing.close,Returns(window_length=126)]
window_length = 252
def compute(self, today, assets, out, prices, returns):
out[:] = ((prices[-21] - prices[-252])/prices[-252] -
(prices[-1] - prices[-21])/prices[-21]) / np.nanstd(returns, axis=0)
class Retur(CustomFactor):
inputs = [USEquityPricing.close]
window_length = 21
def compute(self, today, assets, out, close):
out[:] = (close[-1] - close[0])/close[-1]
"""
def long_pipeline():
mom = momentum()
peg_ratio = Fundamentals.peg_ratio.latest
universe = mom.top(50) & (peg_ratio < 1.5) #Buy top momentum stocks which are at fair value
return Pipeline(
columns={'Factor': peg_ratio},
screen = universe
)
def short_pipeline():
ret = Retur()
peg_ratio = Fundamentals.peg_ratio.latest
universe = ret.top(50) & (peg_ratio > 3) #Short best monthly performing stock wich are Expensive
return Pipeline(
columns={'Factor': peg_ratio},
screen = universe
)
"""
def before_trading_start(context, data):
context.long_pipe = algo.pipeline_output('long')
context.short_pipe = algo.pipeline_output('short')
def rebalance(context, data):
index_history = data.history(context.index_id,"close",context.index_average_window,"1d") #get index data
index_sma = index_history.mean() # Average of index history
current_index = index_history[-1] # get last element
# bull_market = current_index > index_sma #create regime filter ... for MARKET (Marcos' original)
"""
if bull_market: #Only goes long in bull markets
longs = context.long_pipe.Factor.sort_values(ascending=False,)
buy_list = longs[:context.number_of_stocks]
for security in context.portfolio.positions:
if (security not in buy_list): #Sells current positions
order_target(security, 0.0)
for security in buy_list.index:
if len(buy_list) > 5: #Doesn't allocate more than 20% to a single stock
weight = (1.0 / len(buy_list))
else:
weight = 0.2
order_target_percent(security, weight)
else: #Only goes Short in bear markets
shorts = context.short_pipe.Factor.sort_values(ascending=False,)
sell_list = shorts[:context.number_of_stocks]
for security in context.portfolio.positions:
if(security not in sell_list): #Sells current positions
order_target(security, 0.0)
for security in sell_list.index:
if len(sell_list) > 5: #Doesn't allocate more than 20% to a single stock
weight = (1.0 / len(sell_list))
else:
weight = 0.2
order_target_percent(security, -weight)
"""
#=================================================================
def make_pipeline():
# A function to create and return our dynamic stock selector (pipeline). Documentation at https://www.quantopian.com/help#pipeline-title
# Break this piece of logic out into its own function to make it easier to test and modify in isolation. In particular, this function can be copy/pasted into research and run by itself.
universe = QTradableStocksUS()
# Factor: Yesterday's close price
yesterday_close = USEquityPricing.close.latest
#Factors:
#--------
mom = momentum()
ret = Retur()
peg_ratio = Fundamentals.peg_ratio.latest
# Factor: Combined Rank
#----------------------
# By applying a mask to the rank computations, remove any stocks that failed to meet our initial criteria **BEFORE** computing ranks. This means that the stock with rank 10.0 is the 10th-lowest stock that was included in the Q1500US.
# Construct a Factor representing the rank of each asset. Aggregate them together using simple addition after zscoring each item
combined_factor = (
1.00 * mom.zscore()
+ 1.00 * ret.zscore()
- 1.00 * peg_ratio.zscore()
)
# Now, instead of bull & bear MARKETS, want to consider both long & short stocks simultaneously
# longs = context.long_pipe.Factor.sort_values(ascending=False,)
# buy_list = longs[:context.number_of_stocks]
# shorts = context.short_pipe.Factor.sort_values(ascending=False,)
# sell_list = shorts[:context.number_of_stocks]
# FILTERS representing the top and bottom NUM_POSITIONS stocks by our combined ranking system.
# Use these as our tradeable universe each day or other re-allocation period
longs = combined_factor.top(NUM_LONG_POSITIONS, mask=universe)
shorts = combined_factor.bottom(NUM_SHORT_POSITIONS, mask=universe)
# The final output of our pipeline should only include the top/bottom stocks by our criteria
long_short_screen = (longs | shorts)
# Create pipeline
#----------------
pipe = Pipeline(
columns={
'close': yesterday_close,
'longs':longs,
'shorts':shorts,
'combined_factor':combined_factor,
},
screen = (long_short_screen)
)
return pipe
#=====================================================================
def initialize(context):
algo.schedule_function(
rebalance,
algo.date_rules.week_start(days_offset=0),
algo.time_rules.market_open(hours=0,minutes=30)
)
# Create our dynamic stock selector.
algo.attach_pipeline(make_pipeline(), 'pipeline_data')
# attach pipeline for risk model factors to neutralize in optimization
algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')
#=====================================================================
def before_trading_start(context, data):
# Called and runs every day before market open. This is where we get the securities that made it through the pipeline. Call pipeline_output to get the output. Note: this is a dataframe where the index is the SIDs for all securities to pass screen, and the columns are the factors added to the pipeline object above.
context.pipeline_data = algo.pipeline_output('pipeline_data')
print(context.pipeline_data.sort_values('combined_factor')[-10:])
# These are the securities that we are interested in trading each day.
context.security_list = context.pipeline_data.index
# This dataframe will contain all of our risk loadings
context.risk_loadings = algo.pipeline_output('risk_factors')
#=====================================================================
def recording_statements(context, data):
# Plot the number of positions over time (each day).
record(num_positions=len(context.portfolio.positions))
#def my_record_vars(context, data):
# record(leverage = context.account.leverage)
#=====================================================================
def rebalance(context, data):
# Called at the start of every specified schedule period to execute orders according to our schedule_function() timing in order to rebalance the longs & shorts lists.
### Optimize API
pipeline_data = context.pipeline_data
risk_loadings = context.risk_loadings
# Here we define our objective for the Optimize API. We have selected MaximizeAlpha because we believe our combined factor ranking to be proportional to expected returns. This routine will optimize the expected return of our algorithm, going long on the highest expected return and short on the lowest.
objective = opt.MaximizeAlpha(pipeline_data.combined_factor)
### Define the list of constraints
constraints = []
# Constrain our maximum gross leverage
constraints.append(opt.MaxGrossExposure(MAX_GROSS_LEVERAGE))
# Require our algorithm to remain dollar neutral
constraints.append(opt.DollarNeutral())
# Add the RiskModelExposure constraint to make use of the default risk model constraints
neutralize_risk_factors = opt.experimental.RiskModelExposure(
risk_model_loadings=risk_loadings
)
constraints.append(neutralize_risk_factors)
# With this constraint we enforce that NO position can make up greater than MAX_SHORT_POSITION_SIZE on the short side and no greater than MAX_LONG_POSITION_SIZE on the long side. This ensures that we do not overly concentrate our portfolio in one security or a small subset of securities.
constraints.append(
opt.PositionConcentration.with_equal_bounds(
min=-MAX_SHORT_POSITION_SIZE,
max=MAX_LONG_POSITION_SIZE
))
### Put together all the pieces we defined above by passing them into the algo.order_optimal_portfolio function. This handles all of our ordering logic, assigning appropriate weights to the securities in our universe to maximize our alpha with respect to the given constraints.
algo.order_optimal_portfolio(
objective=objective,
constraints=constraints
)
#======================================================================
"""
# Called once at the start of the algorithm.
# Set our slippage and commisions initially 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)
algo.schedule_function(
rebalance,
algo.date_rules.month_start(),
algo.time_rules.market_open(minutes=15),
)
schedule_function(my_record_vars,
date_rules.month_start(),
time_rules.market_close())
set_commission(commission.PerShare(cost=0.013, min_trade_cost=1.3))
# Create our dynamic stock selector.
algo.attach_pipeline(long_pipeline(),'long')
algo.attach_pipeline(short_pipeline(),'short')
context.number_of_stocks = 10 # max number of stock to buy.
context.index_id = sid(8554) # identifier for the SPY. used for trend filter.
context.index_average_window = 100 # moving average periods for index filter
"""