Back to Community
Third-Party Challenge algo attempt

Here's my initial attempt at an algo for:

https://www.quantopian.com/posts/$10k-third-party-challenge-design-a-factor-for-a-large-us-corporate-pension

Comments/questions/improvements welcome.

Clone Algorithm
47
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# https://www.quantopian.com/papers/risk
# Challenge backtest range: 01/04/2014 to 08/29/2018

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.factors import Returns, SimpleBeta, SimpleMovingAverage
import quantopian.optimize as opt
from quantopian.pipeline.filters import QTradableStocksUS, StaticAssets
from quantopian.pipeline.classifiers.morningstar import Sector
import pandas as pd

WINDOW_LENGTH_REGRESS = 253 # days, length of window for returns forecast model
N_STOCKS = 250 # equal long & short, total positions: 2*NSTOCKS

def normalize(x):
    
    r = x - x.mean()
    
    return r/r.abs().sum()

def factor_pipeline():
    
    QTU = QTradableStocksUS()
    
    sectors = [101,102,103,104,205,206,207,308,309,310,311]
    
    returns = Returns(window_length=2, mask=QTU|StaticAssets(symbols('SPY')))
    
    beta = SimpleBeta(target=sid(8554),regression_length=WINDOW_LENGTH_REGRESS,allowed_missing_percentage=1.0)
    
    return_SPY = SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=20)
    returns_average = SimpleMovingAverage(inputs=[returns],window_length=20)
    
    pipeline_columns = {}
    for s in sectors:
        
        alpha = SimpleMovingAverage(inputs=[returns],window_length=WINDOW_LENGTH_REGRESS,mask=QTU&Sector().eq(s))-beta*SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=WINDOW_LENGTH_REGRESS)
        
        returns_forecast = beta*return_SPY + alpha
        
        pipeline_columns['sector_'+str(s)] = (returns_forecast-returns_average).zscore()
    
    pipe = Pipeline(columns = pipeline_columns, screen = QTU)
    
    return pipe
    
def initialize(context):    
    
    attach_pipeline(factor_pipeline(), 'factor_pipeline')
    
    # Schedule my rebalance function
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      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)
    
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

def recording_statements(context, data):
 
    record(num_positions=len(context.portfolio.positions))
    record(leverage=context.account.leverage)
    
def rebalance(context, data):
    
    alpha = pipeline_output('factor_pipeline').sum(axis=1).dropna()
    
    alpha = normalize(pd.Series().append(alpha.nlargest(N_STOCKS)).append(alpha.nsmallest(N_STOCKS)))
    
    objective = opt.TargetWeights(alpha)
       
    order_optimal_portfolio(objective=objective,
                            constraints=[]
                           )
There was a runtime error.
6 responses

Here's the evaluation notebook. Uniqueness score = 92.83%. Turnover exceeds the 20% upper limit.

Loading notebook preview...

@Grant, nice first cut. Try not to limit trading to 500 stocks and trade the entire QTU. See if volatility and turnover is tempered.

Hi @Grant,

Thanks for sharing. Interesting approach! To forecast stock returns by splitting alpha and market returns taking into account how the stock is correlated to the S&P.
There is a thing that is confusing me though. Why you iterate over the sectors to compute alpha and then sum axis=1?

    for s in sectors:  
        alpha = SimpleMovingAverage(inputs=[returns],window_length=WINDOW_LENGTH_REGRESS,mask=QTU&Sector().eq(s))-beta*SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=WINDOW_LENGTH_REGRESS)  
        returns_forecast = beta*return_SPY + alpha  
        pipeline_columns['sector_'+str(s)] = (returns_forecast-returns_average).zscore()  

And then:

def rebalance(context, data):  
    alpha = pipeline_output('factor_pipeline').sum(axis=1).dropna()  

I would have expected this to behave similar to:

QTU = QTradableStocksUS()  
 sectors = [101,102,103,104,205,206,207,308,309,310,311]  
 returns = Returns(window_length=2, mask=QTU|StaticAssets(symbols('SPY')))  
 beta = SimpleBeta(target=sid(8554),regression_length=WINDOW_LENGTH_REGRESS,allowed_missing_percentage=1.0)  
 return_SPY = SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=20)  
 returns_average = SimpleMovingAverage(inputs=[returns],window_length=20)  
 pipeline_columns = {}  
 alpha = SimpleMovingAverage(inputs=[returns],window_length=WINDOW_LENGTH_REGRESS,mask=QTU)- beta*SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=WINDOW_LENGTH_REGRESS)  
 returns_forecast = beta*return_SPY + alpha  
 pipeline_columns['forecast'] = (returns_forecast-returns_average).zscore()  
 pipe = Pipeline(columns = pipeline_columns, screen = QTU)  
 return pipe  

*The code, just removes the iteration and the sector comparison from yours.
But the numbers are quite different. So why iterate over the sectors? How does the mask affect to the SMA for computing the alpha?

Thanks in advance.

Hi Marc -

Glad that you played around with the algo! I think the difference is that summing factor z-scores by sector is different than ignoring sectors and z-scoring the factor across the entire QTU. If one has 9 thingys, and scores them 1-9, this is quite different from breaking the thingys into 3 groups, and scoring within each group. Then one has three things of score 1, three of score 2, and three of score 3.

Hi Grant,

Thanks for clarifying. Understood the concept but was not getting a part of the code so moved it to a Notebook to play with it. Sharing it in case someone wants to mess with it!

Loading notebook preview...

Here is algo modified using the entire universe, the turnover is ~30%, still above the contest criteria.

Clone Algorithm
9
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# https://www.quantopian.com/papers/risk
# Challenge backtest range: 01/04/2014 to 08/29/2018

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.factors import Returns, SimpleBeta, SimpleMovingAverage
import quantopian.optimize as opt
from quantopian.pipeline.filters import QTradableStocksUS, StaticAssets
from quantopian.pipeline.classifiers.morningstar import Sector
import pandas as pd

WINDOW_LENGTH_REGRESS = 253 # days, length of window for returns forecast model

def normalize(x):
    
    r = x - x.mean()
    
    return r/r.abs().sum()

def factor_pipeline():
    
    QTU = QTradableStocksUS()
    
    sectors = [101,102,103,104,205,206,207,308,309,310,311]
    
    returns = Returns(window_length=2, mask=QTU|StaticAssets(symbols('SPY')))
    
    beta = SimpleBeta(target=sid(8554),regression_length=WINDOW_LENGTH_REGRESS,allowed_missing_percentage=1.0)
    
    return_SPY = SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=20)
    returns_average = SimpleMovingAverage(inputs=[returns],window_length=20)
    
    pipeline_columns = {}
    for s in sectors:
        
        alpha = SimpleMovingAverage(inputs=[returns],window_length=WINDOW_LENGTH_REGRESS,mask=QTU&Sector().eq(s))-beta*SimpleMovingAverage(inputs=[returns[sid(8554)]],window_length=WINDOW_LENGTH_REGRESS)
        
        returns_forecast = beta*return_SPY + alpha
        
        pipeline_columns['sector_'+str(s)] = (returns_forecast-returns_average).zscore()
    
    pipe = Pipeline(columns = pipeline_columns, screen = QTU)
    
    return pipe
    
def initialize(context):    
    
    attach_pipeline(factor_pipeline(), 'factor_pipeline')
    
    # Schedule my rebalance function
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      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)
    
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

def recording_statements(context, data):
 
    record(num_positions=len(context.portfolio.positions))
    record(leverage=context.account.leverage)
    
def rebalance(context, data):
    
    alpha = pipeline_output('factor_pipeline').sum(axis=1).dropna()
    
    alpha = normalize(alpha)
    
    objective = opt.TargetWeights(alpha)
       
    order_optimal_portfolio(objective=objective,
                            constraints=[]
                           )
There was a runtime error.