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

36
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.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():

sectors = [101,102,103,104,205,206,207,308,309,310,311]

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:

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)

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.

1

@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:
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]
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 = {}
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?

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!

0

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

3
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.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():

sectors = [101,102,103,104,205,206,207,308,309,310,311]

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:

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)

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.