Back in November we announced that the first version of Quantopian's Risk Model was being made available, for free, to our community. While we provided a set of implementation notes, it was always our intention to follow up with a white paper that would present our approach and implementation a bit more formally. The Quantopian Risk Model white paper is now available on our platform or if you prefer, you can download it as a PDF.

If you haven't taken a look at our risk model or don't know much about risk management, now is a great time to learn! If the white paper isn't your speed quite yet, you can also head over to the lectures page and look at the lessons on Factor Risk Exposure and Risk Constrained Portfolio Optimization.

Recently at QuantCon, I gave a talk describing the risk model. For those who purchase the replay tickets or attended the conference live, you can view the recording today (along with all of the other recordings from QuantCon). This talk will be released to the general public later this year.

We expect to continually work to expand and improve both the Quantopian Risk Model itself and our documentation and educational materials covering risk management and performance attribution. Please let us know if you have questions or feedback!

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

51 responses

First! Great paper. I appreciate the transparency

Excellent! Thank you!

Thank you! Well written!

Hi @Rene,

Thanks again to you and everyone else putting this together. I have just one question [edited]:

1. The Size and Volatility factors may be more 'absolute' (only one way of calculating, although I suppose you could measure volatility of prices rather than returns as well?), but for Value, Momentum, and Short_term_reversal, there are of course many other ways of defining these factors. For example, while other Value factors may be heavily correlated with the one you've defined (book to price essentially), is it really correct to call any residual returns 'alpha'? If one is using other Value factors (e.g. fcf_yield, earnings_yield, sales_yield, fcf_to_ev, etc) wouldn't one still have exposure to the Value style even after constraining exposure to your Value factor (book_to_price)? Same thing with Momentum and Short_term_reversal I would have though. Or am I completely off the mark here?

Thanks again!

Rene,

Thanks for publishing the paper and your work on the risk factor and automating the whole process.

I had a few questions about risk model in general. In the new backtest I can see that it plots common, specific and total returns. Based on the paper the specific component is the alpha.

In the common returns I noted that you are summing up the contributions of each individual risk factor that you have identified to constitute the common risk. What happens if the composition of common risk varies over time in a random manner. If the portfolio made money over a period of time there is bound to be some unintended exposures. Are you able to account for that in the risk model. I guess a consistent outlier is the only case where you can say a particular risk is present in the model? Am I right about this?

One thing I am trying to get a handle on is if common return composition varies over time then is it still a risk, because a risk is something that you get exposed to when you hold onto something with the knowledge that you are incurring a risk (say market risk). I buy market index etf SPY and hold it for a year, I can be certain I exposed myself to market risk and volatility for a full year. If the common risk composition changes over time - isn't the algorithm doing something clever to generate returns by timing and managing the exposure to those risk factors in various periods. At that point can we still say it is the same type of risk as in my earlier example of buying and holding SPY for a full year with a fixed exposure and no timing of entry or exits or varying the exposure in an manner?

Thanks,
Leo

@Leo I struggled with the same question, but never managed your eloquence. I guess the challenge with the market timing argument is that your algo may have fluked it a few times. Do you have enough market timing calls to be statistically significant, especially over a short OOS period like 6 months?

@Burrito Dan, I am trying to suggest is that can they look at the entirety of the backtest and determine if there is statistically significant data to suggest a risk factor should or shouldn't comprise common returns. Using that information they can recalculate common returns differently rather than algebraically summing at each point in time, which is where they lose the notion of how individual risk factor component varied over time and if it is relevant or not and to what degree.

Thank you guys! Great work.

@Leo - ah got it. Good point. I wonder if they do this anyway, as a qualitative step when reviewing algos for entry into the fund? Does the algo have a consistent exposure that should be constrained away, or does it have a time varying exposure whereby additional constraint damages performance?

But anyway, I can see their challenge. Even if your algo has a time varying risk exposure to say momentum, there will still be times when it is somewhat long momentum. Their entire fund portfolio, of which your algo is just part, will probably need to be momentum neutral. If you are giving them long exposure, and no one else has short exposure, how do they hit their fund constraints?

I guess my question boils down to the contest constraints : are you interested in finding algorithms that don't violate the style and sector risk factors at a certain threshold that is defined in the contest or you are looking for pure alpha strategies. The contest constraints seem to indicate the risk factors are more of a filter and that score is volatility adjusted total daily return once you have passed the individual filters.

But if we go with the momentum neutral fund algo construction then would that be possible with the current contest constraints or is that something they would then decide at the allocation phase by deep diving into individual exposure levels of the algorithm ie common returns.

If an algorithm can pick up unintended exposures going forward is it even possible to aim for momentum neutral or any other risk factor neutral fund through a union of independent algorithms? Wouldn't a goal of risk factor constrained to some specified limits (like that specified in the contest) be a more pragmatic and achievable goal?

Thanks for all the great questions and discussion on this thread already!

@Joakim yes, you are describing a known limitation of the risk model as currently formulated - we have only a single valuation ratio holding the place of "Value" where commercial models tend to grow far more complex and include many ratios. In the future we'll continue to improve the model and in the meantime we're certainly cognizant of the limitations in our analysis internally.

@Leo M I think your core question is around how and whether we think about persistent risk differently than time varying spikes/valleys in exposure to a given risk factor. We definitely do view them differently but it can be challenging to capture that complexity in a set of contest rules. Today the constraints of our investment process really do mean that we are specifically most interested in strategies that are "pure alpha" to the extent that they both make consistent returns idiosyncratic to the risk model AND maintain limited exposures to the risk factors in the model. I think there is a chance in the future that we will be able to widen our search process to consider strategies that can do the former, but may not meet the latter. Please clarify or re-ask if I didn't get at what you're asking about.

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Hi Jess,

Yes, I was somewhat trying to determine what in mathematical terms would be viewed as "consistent returns" by the Quantopian investment team with regard to your statement "consistent returns idiosyncratic to the risk model AND maintain limited exposures to the risk factors in the model" because there are limits we set in our algorithm to constrain risk factor exposure say sector and style and there is intended and unintended exposures for each risk factor that sum up across all risk factors to comprise the common returns.

By "consistent returns idiosyncratic to the risk model" I am assuming this is specific returns as a percentage of total returns over time. Is there a threshold or guidance that can be provided in terms of what will be considered "pure alpha strategies" (greater than 50% specific return consistently, greater than 75% specific return consistently, greater than 90% specific return consistently etc.).

My hypothesis is that limiting risk factor exposures itself might not be enough since the individual thresholds for sector (20%) and style (40%) are set wide enough that we might still end up with significant percentage of unintended total common returns as part of our strategy. So I guess a rough estimate at the specific return percentage level as to what will be interesting from allocation point of view can help us at alpha combination time and strategy development time. Not sure if I am overthinking this part and maybe I do not need to be concerned about it and just make sure risk factor exposures are contained within some limits in the algorithm and leave it at that.

Thanks,
Leo

It is quite understandable that every model will have its limitations because they are just an abstract representation of the real world and it is impossible to capture all the factor nuances that contribute to risks. All risks and returns, for that matter, varies over time (non stationary) and highly dependent on market conditions. Having said that and as an unsolicited suggestion on how to improve the risk model, I would rather see style risks factors be more adaptive in nature to be more attuned to point in time market conditions. This would lessen the variance on over/under estimation of risks.

As a concrete example, Short Term Reversal Style Risks is currently calculated using a one day lag of the negative 14 day RSI. Why is the lookback period pegged at 14 days? During the Global Financial Crisis (GFC) of 2008-2009, the mean reversion strategy using 2 day RSI made enormous amounts of profits because volatility was high and fast. Under a fixed 14 day lookback of RSI, STR risks are understated. Conversely, during low volatility and trending market conditions like 2016-2017, STR risks are overstated. So perhaps, a volatility adjusted lookback period of RSI will be more adaptive and accurate means of measuring STR risks.

Yet another way to make improvements is to make the style risks thresholds variable, again, depending on market conditions as opposed to an across the board limit of 40%. As in my example above, you can allow for STR to go up to 50% while reducing Momentum to 30% during periods of high volatility such as the GFC. During low volatility/trending periods, allow momentum risks to go up to 50% while reducing STR to 30%. Just back of the envelope suggestions. I think this way, one will be able to extract more returns without sacrificing risk mitigation. It is how close you measure the risk factors accurately vis a vis reality that ultimately brings the desired results.

@Leo M "Not sure if I am overthinking this part and maybe I do not need to be concerned about it and just make sure risk factor exposures are contained within some limits in the algorithm and leave it at that." Yes, that would be my advice :)

@James I'll leave to Rene to weigh in on those suggestions, they sound like interesting things to check. Personally I have strong bias against increasing model complexity, but I agree with you that regime changes are real and substantial. We've separately done some volatility regime modeling work that hasn't been folded into the risk model yet - but I think will make its way onto our blog/forums shortly and I'd love your feedback when we do share that.

Thanks @Jess, good to be aware of. I suppose we can always load our own custom FactorExposure as a constraint in Optimize API. I just need to figure out how to do that first.

@Jess, thanks for your feedback and will await Rene's comments. I have also worked on volatility regime and other regime change modeling, would be very interested to see your work and share my thoughts. Good work!

Jess, thanks for your advice. I was unnecessarily concerned about debugging and eliminating all common returns from my algorithm and wasn't able to articulate that well earlier in the thread.

Thanks Rene -

I did a first-pass read and will need to take more time to digest fully the details. One quick question: why do you show separately the computation for the complementary stocks (the section "Style factor exposures of complementary stocks ")? Is this just for completeness? Or is the result needed later on in the section "Risk Calculation"? I'm just trying to understand why we care about the stocks that are not in the estimation universe (presumably, for example, the QTradableStocksUS)?

I'd asked the question before, but what is the rationale in not including interaction terms in the basic model under "Mathematical Model"? Did you already determine that interactions aren't important?

Also, I'm wondering why you don't perform tests of statistical significance on the factors. I think conventionally, one would refine a factor model by dropping terms that are not statistically significant. You retain all terms. Wouldn't it be better to drop terms that are just noise? Or perhaps you've done this analysis and have already pruned the model down to only factors that are statistically significant. As I recall from doing this sort of thing for Design of Experiments/Response Surface Methodology, one always wants to refine the model, excluding terms in the model that are not statistically explanatory, and then re-run the factor analysis.

Your white paper doesn't suggest there is anything particularly fancy and proprietary under the hood. Would you be able to publish the code? Presumably it is implemented in Pipeline. If the code cannot be shared, perhaps I'll endeavor to write the code myself, at some point, time permitting. Or if another Q user has already attempted this, I'd be interested in reviewing the code.

How far can we go, or more appropriately, how critical can we get concerning the white paper and its ramifications?

@ Guy - I don't think you need their permission; it is implied by their publishing it on an open forum. I'd assume feedback is welcome. If you have technical criticism, let's hear it. And if you have a proposal for an entirely different and potentially better approach, that would be interesting, too. Just steer clear of anything regarding the Q "business model" as such topics are generally not welcome on the forum (at least from me).

@ Grant, sorry, I have been unable to post my reply for the past few days. Either I get an error message saying: “sorry, something went wrong. ”, or, a “pending review” which should translate to some kind of censuring procedure.

You assumed that feedback would be welcomed, or that I did not need permission. It might appear that it might not be the case. I know, I pre-announced I would be critical. On the other hand, maybe the error messages were just coincidental for some unknown reason.

Could you PM me, and I will attach the HTML file to my reply.

Sorry for the delay.

I have been having difficulty posting via mobile but from a desktop pc it works.

Then, I will try again.
[edited]. Failed again. Apparently, does not work on all desktop pcs.

[re-edited]. Would appear I can put a word in, and then re-edit. Will try again.

Prepared message.

@ Grant, I have a note I have been holding back for the past few weeks.

The white paper emphasized the purpose for which it was released: market-neutral and beta-neutral trading strategies. I find these to have considerable inherent drawbacks. Not for what is described in the paper. We can all shape our trading strategies to do whatever we want. It is more about what the outcome of a market-neutral strategy will produce profitwise over the long term.

When you put the equations in place it would appear that all you can get might be at most the long-term market drift where you do not need to do anything to get it anyway. It is almost provided for free. As an “illusory” gift for participating in the game. Evidently, if you do not play well, you can lose that too.

The final equations in that appraisal might be a major blow for market-neutral trading strategies. We should question the usefulness of these strategies if not their very nature.

When you use equations to describe what you see, it becomes very restrictive. Not because you cannot put equations on the table, but because you use an equal sign which makes it quite a categorical and unequivocal statement.

Follow the argumentation in this HTML FILE, and you should reach the same conclusions as in the ending market neutrality section.

[Edited] It worked.

Attached is an attempt at recreating the Risk factors (as per the white paper) as Alpha factors in an algo. I likely got a few of them wrong (Volatility is likely wrong and possibly Momentum) so if anyone could correct them and repost, I'd be very grateful! Not sure why Short_term_reversal exposure is way above the 40% constraint (it's using all default constraint parameters).

Many thanks to @Karl for collaborating and helping to fix an NA issue (winsorizing seems to have fixed it).

6
Backtest from to with initial capital
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
"""
This algorithm demonstrates the concept of long-short equity. It uses a
combination of factors to construct a ranking of securities in a liquid
tradable universe. It then goes long on the highest-ranked securities and short
on the lowest-ranked securities.

For information on long-short equity strategies, please see the corresponding
lecture on our lectures page:

https://www.quantopian.com/lectures

This algorithm was developed as part of Quantopian's Lecture Series. Please
direct and questions, feedback, or corrections to [email protected]
"""
import numpy as np
import quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import AnnualizedVolatility, RSI, CustomFactor, Returns

from quantopian.pipeline.data import Fundamentals, USEquityPricing

# Constraint Parameters
MAX_GROSS_LEVERAGE = 1.0
TOTAL_POSITIONS = 800

# Here we define the maximum position size that can be held for any
# given stock. If you have a different idea of what these maximum
# sizes should be, feel free to change them. Keep in mind that the
# optimizer needs some leeway in order to operate. Namely, if your
# maximum is too small, the optimizer may be overly-constrained.
MAX_SHORT_POSITION_SIZE = 2.0 / TOTAL_POSITIONS
MAX_LONG_POSITION_SIZE = 2.0 / TOTAL_POSITIONS

def initialize(context):
"""
A core function called automatically once at the beginning of a backtest.

Use this function for initializing state or other bookkeeping.

Parameters
----------
context : AlgorithmContext
An object that can be used to store state that you want to maintain in
your algorithm. context is automatically passed to initialize,
before_trading_start, handle_data, and any functions run via schedule_function.
context provides the portfolio attribute, which can be used to retrieve information
"""

algo.attach_pipeline(make_pipeline(), 'long_short_equity_template')

# Attach the pipeline for the risk model factors that we
# want to neutralize in the optimization step. The 'risk_factors' string is
# used to retrieve the output of the pipeline in before_trading_start below.

# Schedule our rebalance function
algo.schedule_function(func=rebalance,
date_rule=algo.date_rules.every_day(),
time_rule=algo.time_rules.market_open(),
half_days=True)

# Record our portfolio variables at the end of day
algo.schedule_function(func=record_vars,
date_rule=algo.date_rules.every_day(),
time_rule=algo.time_rules.market_close(),
half_days=True)

def make_pipeline():
"""
A function that creates and returns our pipeline.

We 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.

Returns
-------
pipe : Pipeline
Represents computation we would like to perform on the assets that make
it through the pipeline screen.
"""
# The factors we create here are based on fundamentals data and a moving
# average of sentiment data

size = np.log(Fundamentals.market_cap.latest)
value = (Fundamentals.total_equity.latest / Fundamentals.market_cap.latest)
short_term_rev = -1.0 * RSI(window_length=15, mask=universe)

###################################################################

# Winsorize outliers: https://en.wikipedia.org/wiki/Winsorizing
momentum_winsorized = momentum.winsorize(min_percentile=0.05, max_percentile=0.95)
size_winsorized = size.winsorize(min_percentile=0.05, max_percentile=0.95)
value_winsorized = value.winsorize(min_percentile=0.05, max_percentile=0.95)
short_term_rev_winsorized = short_term_rev.winsorize(min_percentile=0.05, max_percentile=0.95)
volatility_winsorized = volatility.winsorize(min_percentile=0.05, max_percentile=0.95)

combined_factor = (
momentum_winsorized.zscore() +
size_winsorized.zscore() +
value_winsorized.zscore() +
short_term_rev_winsorized.zscore() +
volatility_winsorized.zscore()
)

###################################################################

# combined_factor = (
#     momentum.zscore() +
#     size.zscore() +
#     value.zscore() +
#     short_term_rev.zscore() +
#     volatility.zscore()
# )

# combined_factor = np.where(combined_factor.isfinite(), combined_factor, 0)

# Build Filters representing the top and bottom baskets of stocks by our
# combined ranking system. We'll use these as our tradeable universe each
# day.

# The final output of our pipeline should only include
# the top/bottom 300 stocks by our criteria
long_short_screen = (
(longs | shorts)
& momentum.notnull() # & momentum.notnan()
& size.notnull() # & size.notnan()
& short_term_rev.notnull() # & short_term_rev.notnan()
& volatility.notnull() # & volatility.notnan()
& combined_factor.notnull() # & combined_factor.notnan()
)

# Create pipeline
pipe = Pipeline(
columns={
'longs': longs,
'shorts': shorts,
'combined_factor': combined_factor
},
screen=long_short_screen # & combined_factor.notnan() & combined_factor.notnull() & combined_factor.isfinite()
)
return pipe

"""
Optional core function called automatically before the open of each market day.

Parameters
----------
context : AlgorithmContext
See description above.
data : BarData
An object that provides methods to get price and volume data, check
whether a security exists, and check the last time a security traded.
"""
# Call algo.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 = algo.pipeline_output('long_short_equity_template')

def record_vars(context, data):
"""
A function scheduled to run every day at market close in order to record
strategy information.

Parameters
----------
context : AlgorithmContext
See description above.
data : BarData
See description above.
"""
# Plot the number of positions over time.
algo.record(num_positions=len(context.portfolio.positions))

# Called at the start of every month in order to rebalance
# the longs and shorts lists
def rebalance(context, data):
"""
A function scheduled to run once every Monday at 10AM ET in order to
rebalance the longs and shorts lists.

Parameters
----------
context : AlgorithmContext
See description above.
data : BarData
See description above.
"""
# Retrieve pipeline output
pipeline_data = context.pipeline_data.dropna(how='any')

# 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(
version=0
)
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.
try:
algo.order_optimal_portfolio(
objective=objective,
constraints=constraints
)
except Exception as message: log.warn("Error: {}".format(message))

class Momentum(CustomFactor):
""" Momentum factor """
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)
There was a runtime error.

For the risk model style factors, it would be interesting to run them through Alphalens individually, to see if they are actual predictive factors, or just noise. If the latter, I would question their value in the risk model. One of the super-users here (Luca), did something similar awhile back; I just can't put my finger on it. Now that we have complete mathematical definitions of the factors, it might be worth re-visiting.

@Joakim, you should not be surprised with the results. First, everything you did in that strategy, you can do. But, the point is not there. The point is: is the strategy structured to be profitable by design. And there, I would have to conclude no.

In a way, you confirm the last equation in my argumentation where it is stated that the expected outcome of such procedures is: E[F(t)] = F(0) – Σ(Exp). However, there are things you can do that will improve the design even if it will move you away from the dollar-neutral stance adopted by that strategy.

For one, disabling “  constraints.append(opt.DollarNeutral()) ” will improve performance. Simply comment it out, and the strategy will show a profit. However, the beta will rise. And the strategy will not meet the contest's dollar-neutral criterion.

To get more profits, but still no real alpha, you could change the impact of the factors used. I do not think that all factors should be treated equally, I do think that the market is 50% up and 50% down, nor do I think that all bets on each stock should be considered equal. You can always program a trading script to be all that, but it will be a contraption of your design where you try to force the price series through the hoops. Note that removing the dollar-neutrality, the strategy will be biased to the long side since the long side will be serviced first.

You used $25k as the trading unit. But set the total number of stocks to 800 which would suggest that$20M was made available. By going dollar-neutral, you forced your strategy to use only 200 stocks on each side making it use $10M. But still, your program was acting as if on leverage 2x instead of 1x, even though 1x was actually recorded. If your bets are the same size as if leveraged 2x, but you make half the bets, could the strategy still be considered unleveraged? Even if there was no leverage used. Maybe the attached might help where I removed the dollar-neutral thing, and changed some of the factor weights. 5 Loading... Backtest from to with initial capital 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 """ This algorithm demonstrates the concept of long-short equity. It uses a combination of factors to construct a ranking of securities in a liquid tradable universe. It then goes long on the highest-ranked securities and short on the lowest-ranked securities. For information on long-short equity strategies, please see the corresponding lecture on our lectures page: https://www.quantopian.com/lectures This algorithm was developed as part of Quantopian's Lecture Series. Please direct and questions, feedback, or corrections to [email protected] """ import numpy as np import quantopian.algorithm as algo import quantopian.optimize as opt from quantopian.pipeline import Pipeline from quantopian.pipeline.factors import AnnualizedVolatility, RSI, CustomFactor, Returns from quantopian.pipeline.filters import QTradableStocksUS from quantopian.pipeline.experimental import risk_loading_pipeline from quantopian.pipeline.data import Fundamentals, USEquityPricing # Constraint Parameters MAX_GROSS_LEVERAGE = 1.0 TOTAL_POSITIONS = 800 # Here we define the maximum position size that can be held for any # given stock. If you have a different idea of what these maximum # sizes should be, feel free to change them. Keep in mind that the # optimizer needs some leeway in order to operate. Namely, if your # maximum is too small, the optimizer may be overly-constrained. MAX_SHORT_POSITION_SIZE = 2.0 / TOTAL_POSITIONS MAX_LONG_POSITION_SIZE = 2.0 / TOTAL_POSITIONS def initialize(context): """ A core function called automatically once at the beginning of a backtest. Use this function for initializing state or other bookkeeping. Parameters ---------- context : AlgorithmContext An object that can be used to store state that you want to maintain in your algorithm. context is automatically passed to initialize, before_trading_start, handle_data, and any functions run via schedule_function. context provides the portfolio attribute, which can be used to retrieve information about current positions. """ algo.attach_pipeline(make_pipeline(), 'long_short_equity_template') # Attach the pipeline for the risk model factors that we # want to neutralize in the optimization step. The 'risk_factors' string is # used to retrieve the output of the pipeline in before_trading_start below. algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors') # Schedule our rebalance function algo.schedule_function(func=rebalance, date_rule=algo.date_rules.every_day(), time_rule=algo.time_rules.market_open(), half_days=True) # Record our portfolio variables at the end of day algo.schedule_function(func=record_vars, date_rule=algo.date_rules.every_day(), time_rule=algo.time_rules.market_close(), half_days=True) def make_pipeline(): """ A function that creates and returns our pipeline. We 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. Returns ------- pipe : Pipeline Represents computation we would like to perform on the assets that make it through the pipeline screen. """ universe = QTradableStocksUS() # The factors we create here are based on fundamentals data and a moving # average of sentiment data momentum = Momentum(mask=universe) size = np.log(Fundamentals.market_cap.latest) value = (Fundamentals.total_equity.latest / Fundamentals.market_cap.latest) short_term_rev = -1.0 * RSI(window_length=15, mask=universe) volatility = -AnnualizedVolatility(window_length=126, mask=universe) ################################################################### # Added winsorize(): 2018.05.28 # Winsorize outliers: https://en.wikipedia.org/wiki/Winsorizing momentum_winsorized = momentum.winsorize(min_percentile=0.05, max_percentile=0.95) size_winsorized = size.winsorize(min_percentile=0.05, max_percentile=0.95) value_winsorized = value.winsorize(min_percentile=0.05, max_percentile=0.95) short_term_rev_winsorized = short_term_rev.winsorize(min_percentile=0.05, max_percentile=0.95) volatility_winsorized = volatility.winsorize(min_percentile=0.05, max_percentile=0.95) combined_factor = ( momentum_winsorized.zscore()*3.0 + size_winsorized.zscore() + value_winsorized.zscore()*2.0 + short_term_rev_winsorized.zscore()*.01 + volatility_winsorized.zscore() ) ################################################################### # combined_factor = ( # momentum.zscore() + # size.zscore() + # value.zscore() + # short_term_rev.zscore() + # volatility.zscore() # ) # combined_factor = np.where(combined_factor.isfinite(), combined_factor, 0) # Build Filters representing the top and bottom baskets of stocks by our # combined ranking system. We'll use these as our tradeable universe each # day. longs = combined_factor.top(TOTAL_POSITIONS//2, mask=universe) shorts = combined_factor.bottom(TOTAL_POSITIONS//2, mask=universe) # The final output of our pipeline should only include # the top/bottom 300 stocks by our criteria long_short_screen = ( (longs | shorts) & momentum.notnull() # & momentum.notnan() & size.notnull() # & size.notnan() & short_term_rev.notnull() # & short_term_rev.notnan() & volatility.notnull() # & volatility.notnan() & combined_factor.notnull() # & combined_factor.notnan() ) # Create pipeline pipe = Pipeline( columns={ 'longs': longs, 'shorts': shorts, 'combined_factor': combined_factor }, screen=long_short_screen # & combined_factor.notnan() & combined_factor.notnull() & combined_factor.isfinite() ) return pipe def before_trading_start(context, data): """ Optional core function called automatically before the open of each market day. Parameters ---------- context : AlgorithmContext See description above. data : BarData An object that provides methods to get price and volume data, check whether a security exists, and check the last time a security traded. """ # Call algo.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 = algo.pipeline_output('long_short_equity_template') # This dataframe will contain all of our risk loadings context.risk_loadings = algo.pipeline_output('risk_factors') def record_vars(context, data): """ A function scheduled to run every day at market close in order to record strategy information. Parameters ---------- context : AlgorithmContext See description above. data : BarData See description above. """ # Plot the number of positions over time. algo.record(num_positions=len(context.portfolio.positions)) # Called at the start of every month in order to rebalance # the longs and shorts lists def rebalance(context, data): """ A function scheduled to run once every Monday at 10AM ET in order to rebalance the longs and shorts lists. Parameters ---------- context : AlgorithmContext See description above. data : BarData See description above. """ # Retrieve pipeline output pipeline_data = context.pipeline_data.dropna(how='any') risk_loadings = context.risk_loadings.dropna(how='any') # 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, version=0 ) 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. try: algo.order_optimal_portfolio( objective=objective, constraints=constraints ) except Exception as message: log.warn("Error: {}".format(message)) class Momentum(CustomFactor): """ Momentum factor """ 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) There was a runtime error. @Joakim, I would like to add. We can make all our bets the same like in your trading strategy where you fixed the initial bet size at$25k. There is nothing wrong with that. However, by setting the bet size, we explicitly say that all bets will be treated equal. $25k in ZNGA having the same value as$25k in AAPL. The bet size might be the same, but the intrinsic value of those bets and their expected outcomes are not.

Also, just by fixing the bet size, you installed return degradation. It is not that your trading strategy's CAGR might decline with time, it is more like it is built in. For this reason, giving more time to that type of trading strategy will see its CAGR decline and this will display on an equity chart as if the strategy was breaking down, producing less and less as time increases.

There are remedies to that type of problem. We can compensate and force the strategy to behave differently.

Hey @Guy,

Thank you! Wow, that's quite interesting that removing the dollar-neutral constraint has such a positive effect on the algo.

To be honest, I just cloned the Long/Short template algo from the lecture series, and left most settings to the default. My main intent was to try to recreate the Risk factors as defined in the Risk White Paper. Since these are all very known factors I didn't expect there to be any money in it, but I wanted to see how the Risk API would handle full (and equal) exposure to all the Risk factors. I also thought that perhaps they could be used to 'neutralize' Risk Style exposure in one of my other algos, rather than constraining exposure in the Optimizer.

I'm pretty bad with formulas unfortunately and I wasn't able to implement the Volatility factor as defined in the paper, so just used the built in Annualized one. This is probably wrong, but I put a minus (-) sign in front of AnnualizedVolatility() essentially because I thought the Volatility Risk exposure was meant to be exposure to LOW volatility, rather than HIGH volatility stocks. Looking at the formula in the paper I'm not able to tell which one it is though. Perhaps you could help advise on this, looking at the formula?

Thanks again.

Include this statement, Joakim to import all the Q Risk Model style factors:

from quantopian.pipeline.experimental import (Momentum, ShortTermReversal, Size, Value, Volatility)


ps: Reference: Quantopian Risk Model In Algorithms to access individual risk factors.

Ah! So the factors are available? Is there documentation on how to use them? Are they formulated as Pipeline built-in factors?

@Grant @Joakim: The individual factors are available (see the full list) and are used in algorithms just like any other Pipeline factor. Here's a simple example:

from quantopian.pipeline import Pipeline
from quantopian.pipeline.experimental import Size

def initialize(context):
pipe = Pipeline()
attach_pipeline(pipe, 'size_factor_pipeline')

context.size_factor_pipeline = pipeline_output('size_factor_pipeline')

def my_method(context, data):
# Access context.size_factor_pipeline in your methods.

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

@Joakim, there is nothing there either.

The payoff matrix Σ(H(a)∙ΔP) of a strategy (a), if not distinguishable from a market average proxy such as (SPY), Σ(H(spy)∙ΔP), only reveals that its forecasting abilities were almost nil, which is what the original trading script showed. At least, you removed the “massaged” psychsignal with its stocktwits.bull_minus_bear signal to give it a chance.

The (spy)'s strategy payoff matrix: Σ(H(spy)∙ΔP) is free. There is no effort to get that. You just wait. I concede, it is a long wait time, but still just waiting. So, what you design must at least outperform the long-term indexer.

Making Σ(H(a)∙ΔP) > Σ(H(spy)∙ΔP) is an obligation, and not just a wish. You need to prove to yourself that your strategy (a) will outperform the index proxy with the tools you have, that is: Σ(H(a)∙ΔP) > Σ(H(spy)∙ΔP) will hold knowing quite well that the future is uncertain. You still have to make sure you are going to win no matter what. At the very least, it has to show that it could have done so over an extended period of time. It must do better than SPY, with a mathematical almost surely.

If your trading strategy only approaches (spy), saying that its payoff matrix is not better than holding (spy), then it is also saying that the forecasting methods or whatever is used to anticipate future prices is not doing its job very well. One simple cause for this is that the factors used do not have that much predictive powers. This can be observed when Σ(H(a)∙ΔP) → Σ(H(spy)∙ΔP) or Σ(H(a)∙ΔP) < Σ(H(spy)∙ΔP).

In the backtest analysis you have the “cumulative return on log scale” chart that will give you at a glance if your strategy is generating real alpha. The green line will be above the SPY benchmark and continuously spread away from it. If below the benchmark, your strategy is working hard to underperform its peers. And, if the negative spread in increasing, the strategy is breaking down and definitely has a degrading CAGR.

The more you constrain a system, the less performance you will be able to extract. And the most constraining constraint will supersede others in the set. It is like if you wanted beta-neutral when dollar-neutral has already done the job.

Abhi -

Are they just Pipeline built-in factors, as described on the help page? Or something else?

Here's a first-cut at trading the risk factors. I comment out the risk model constraint in the optimize API. Interestingly, if I use it, the leverage is ~0.4, so something is amiss, since I'd expect it to be ~1.0. Note also that I use:

set_commission(commission.PerShare(cost=0, min_trade_cost=0))


I'm still trying to get a handle on what risk is being managed. I guess the fact that the algo loses a lot of money would be avoided by avoiding the risk factors. But if the risk factors don't make money in the first place, then wouldn't that be apparent in back-testing? Intuitively, the sector diversification makes sense, I suppose, but the justification for the style risk factors is still lost on me. At one point, the argument I heard was that investors would be unwilling to pay a premium for the risk factors, because they are so common. But if they don't actually make money, then they would not pay for them, because, well, they don't make money.

Maybe there is a way to formulate an algo such that the risk factors do make money? I'd assumed that a sum of z-scores would do the trick, but apparently not.

19
Backtest from to with initial capital
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
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 Momentum, ShortTermReversal, Size, Value, Volatility

MAX_GROSS_EXPOSURE = 1.0
NUM_LONG_POSITIONS = 200
NUM_SHORT_POSITIONS = NUM_LONG_POSITIONS
MAX_POSITION_SIZE = 0.015

MIN_BETA_EXPOSURE = -0.3
MAX_BETA_EXPOSURE = 0.3

def make_pipeline():

beta = SimpleBeta(target=sid(8554),regression_length=260,
allowed_missing_percentage=1.0
)

factors = [Momentum, ShortTermReversal, Size, Value, Volatility]

combined_alpha = None
for f in factors:
if combined_alpha == None:
else:

longs = combined_alpha.top(NUM_LONG_POSITIONS)
shorts = combined_alpha.bottom(NUM_SHORT_POSITIONS)

long_short_screen = (longs | shorts)

pipe = Pipeline(columns = {
'combined_alpha':combined_alpha,
'beta':beta,
},
screen = long_short_screen)
return pipe

def initialize(context):

attach_pipeline(make_pipeline(), 'long_short_equity_template')

# Schedule my rebalance function
schedule_function(func=rebalance,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_open(minutes=60),
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)

context.init = True

context.pipeline_data = pipeline_output('long_short_equity_template')

def recording_statements(context, data):

record(num_positions=len(context.portfolio.positions))
record(leverage=context.account.leverage)

def rebalance(context, data):

pipeline_data = context.pipeline_data

# demean and normalize
combined_alpha = pipeline_data.combined_alpha - pipeline_data.combined_alpha.mean()
combined_alpha = combined_alpha/combined_alpha.abs().sum()

objective = opt.MaximizeAlpha(combined_alpha.dropna())

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(
min_exposures={'beta':MIN_BETA_EXPOSURE},
max_exposures={'beta':MAX_BETA_EXPOSURE}
)
constraints.append(beta_neutral)

# risk_model_exposure = opt.experimental.RiskModelExposure(
# )

# constraints.append(risk_model_exposure)

order_optimal_portfolio(
objective=objective,
constraints=constraints,
)
There was a runtime error.

@Karl, @Abhijeet, @Grant, @Guy,

Oh cool! I didn't realize the Risk Factors are available as built in factors in the API. Thank you!

Could anyone please advise if the Volatility factor is exposure to HIGH volatility stocks, or to LOW volatility stocks? I'm probably wrong but I thought the 'known' risk premium is for exposure to LOW volatility stocks? I can't actually tell from the paper but it looks like it might be measuring exposure to HIGH volatility stocks, based on this statement:

Volatility: The volatility factor captures the difference in returns
between high volatility stocks and low volatility stocks.

Since (high) Volatility is probably the most accepted measurement of Risk, and Volatility is already tracked in the algo, it seems a bit strange (to me anyway) to also track exposure to HIGH volatility stocks (i.e. double measuring HIGH volatility)?

@Guy,

Thanks. I wasn't actually trying to see if there's any alpha in any of these factors (I wasn't expecting any at least). Just wanted to get a better understanding of how the Risk Factors work.

If the strategy was Long only, then I can see that beating the S&P500 index (Russell3000 might be a better benchmark to the QTU universe though) would be a sensible measurement. For a Long/Short strategy however, trying to beat the market is not a meaningful measurement in my view, as the purpose of a Long/Short strategy is to generate Alpha ('risk free' returns) from 'inefficiencies' in the market. A better benchmark might be the USD LIBOR overnight rate (for a daily rebalanced portfolio), or some other bond/FI-based instrument (a slightly longer Govt bond perhaps, to add some risk premium), as market Alpha is not completely risk-free.

Some factors insight might come from swapping short & long. Can be done with a minus sign. Returns of -15 become +8.
'combined_alpha': -combined_alpha,
or
objective = opt.MaximizeAlpha( -combined_alpha.dropna() )

Or in gathering information on the effect of risk factors, they can be flipped individually to find out what happens.
Here I multiplied volatility values by -1 (reversing positive and negative) on line 40. Did not try others.
This also has pipeline preview and some of its output in the source for your convenience.

8
Backtest from to with initial capital
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/posts/risk-model-white-paper-released-and-available-for-your-reading
#
# Changes
#     alpha += -vol
#            'alpha': -alpha,
#        regression_length=      126

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

beta = SimpleBeta(target=sid(8554),regression_length=      126,
allowed_missing_percentage=1.0)

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')

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)

context.pipeline_data = pipeline_output('long_short_equity_template')

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
'''

'''
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      -...
'''

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(
min_exposures={'beta':MIN_BETA_EXPOSURE},
max_exposures={'beta':MAX_BETA_EXPOSURE}
)
constraints.append(beta_neutral)

# risk_model_exposure = opt.experimental.RiskModelExposure(
# )

# constraints.append(risk_model_exposure)

order_optimal_portfolio(
objective=objective,
constraints=constraints,
)

def log_pipe(context, data, z, num, filter=None):
'''
# 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:
log.info('{}{}{}   Series {}  len {}'.format('min' .rjust(pad+5),
))
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 ''
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
i += 1
content += ('\n{}{}{}{}{}'.format(
''
))
for lst in content_min_max[1:]:    # populate content using max lengths
content += ('\n{}{}{}{}     {}'.format(
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
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)


There was a runtime error.

Beautiful code @Blue! I have a lot to learn from you in that regard.

Did you just validate my hypothesis on Volatility? :)

Perhaps so. Meanwhile Size & ShortTermReversal made negative seem best in a current version I tried.

Appreciation for the kind words, Karl & Joakim.

JA, thanks, I also did not know that "Risk Factors are available as built in factors in the API" until you mentioned it. To explain what i mean: For example, in my source above are listed the components of the risk model pipe because pipeline preview is used not just on the regular output but also on 'risk_loading_pipeline' so then looking at those, health_care is one and I tried adding it to the imports as HealthCare and that works.

from quantopian.pipeline.experimental import Momentum, ShortTermReversal, Size, Value, Volatility, HealthCare

At that point if added (like as hth or something) as the others in pipeline one could experiment with increasing its influence in the alpha weights under certain conditions or whatever can be dreamed up. Maybe if investors become spooked over HealthCare or Technology for whatever reason they tend to move money to Utilities et al or or .... Those values were always available in before_trading_start although the import (credit GK maybe) makes for working with them easier and also in pipeline itself. Hoping some will discover reliable benefits there somewhere. Perhaps someone can clarify what the numbers within the risk model represent, as each stock has its values there for each category.

Here they are again, the 16 columns in risk model pipeline: basic_materials, communication_services, consumer_cyclical, consumer_defensive, energy, financial_services, health_care, industrials, momentum, real_estate, short_term_reversal, size, technology, utilities, value, volatility

Interesting points, Grant you raised that keep me thinking:

..the justification for the style risk factors is still lost on me.

Perhaps it is in the uncommon spaces amidst the 5 common styles where prized alphas are yet latent. I can think of an analogue to the "junk DNA" between CGTA where new values are yet to be discovered.

Maybe there is a way to formulate an algo such that the risk factors do make money?

Well they do "make" money if the QRM factors save a dollar by mitigating risks. Extending from your intuitive "formulate an algo", I suppose there are already methods to, for instance, morph the QRM factors into a risk-adjusted portfolio, and append opt.TargetWeights(QRM_risk_adjusted_weights) to opt.MaximiseAlpha(not_QRM_alphas) to perform second stage optimisation into the Q compliant structure?

Just thinking.

@ Karl -

The right way to go about this is to use Alphalens to determine the predictability of the individual Quantopian risk model style risk factors, Momentum, ShortTermReversal, Size, Value, Volatility. One constructive criticism of the white paper is that no analysis of the style risk factors was done to show that they are, indeed, factors. Quantopian has the Alphalens tool for identifying whether a given black box is a factor, or not; it could have been applied to the style risk factors, and the analysis included in the risk model white paper. It is remarkable that Quantopian did not analyze the proposed risk factors using Alphalens. It would be interesting to hear from them as to why, given their regular promotion of the tool to users, it was not applied in this case.

One potential downside of including risk factors in the model that aren't actually factors is unproductive turnover. In applying the optimize API risk model constraint, the alpha vector gets re-jiggered for no reason.

Hi @Grant,

This is the way I look at it, which may or may not be correct (I believe academics still disagree on some of this stuff).

The Risk factors now mainly provide another way of categorizing stocks (into investing styles), though they may have once been quite consistently predictive Alpha factors as well. For example, Value and Size may consistently have outperformed before Fama & French 'discovered' them and published their paper in 1992. Once 'discovered' however, they are unlikely to consistently outperform, but individually they will still likely outperform at certain market periods, underperform during other periods, and just move with the market (non-predictive) at other times.

For example, over exposure to 'Value' stocks in the late 90s probably did very poorly, and under exposure (some may classify this as Growth stocks, though I would disagree) probably outperformed during that period. Until the 2000/2001 dot-com crash that is. Similarly, during recent years, Value perhaps hasn't been very 'predictive' but Momentum and Volatility perhaps has been. Until February this year it seems.

So, for example, if I have a strategy that is consistently overly exposed to 'Value' stocks, and consistently under exposed to 'Size', then essentially I'm making a bet that LargeCap Value stocks will outperform SmallCap non-Value stocks going forward. If I'm right, I win and make money, but if I'm wrong I lose and that's the 'Risk' I take by being overly/underly exposed to these factors.

In short, the trick lies in trying to predict when they will outperform, underperform, or neither going forward. Or in finding new factors that somewhat consistently outperform/is predictive, and at the same time isn't very correlated with any of the 'known' factors. If you can figure that out, you'll be a rich(er) man! ;)

@ Joakim -

There's something missing in the Q risk model. Say that whatever Fama & French discovered in 1992, is still at work in the market today in 2018; Value & Size are predictive, at least transiently. Well, let's say in the next 20 years, Value & Size suffer from the well-known phenomenon of "alpha decay" and are no longer meaningful. Would Q still have them in their risk model? At some point, they become noise, and should be dropped, if I'm thinking about this correctly.

@Grant,

Yes, if the market was completely efficient, then that would be true. Personally I don't believe we're anywhere close to a fully efficient market though, and I believe these factors will continue to over-perform during some periods, underperform during others, and just be noise at other times.

Value for example oftentimes tend to be very 'unsexy' compared to high flying Growth stocks (as was the case in the late '90s), until the market shifts and people realize that there's lots of bargains to be found in the Value universe. Similarly with Size, small-cap stocks can't really be traded by 'real money' so there continues to be more price inefficiencies in the smaller market-cap universe, and therefore Alpha to be found (not always, but sometimes).

With 'alpha decay' a factor will stop working at some point, so people stop using the factor as there's no money to be made from it, at which point the factor may start working again... :)

Well, my point is, there is no discussion in the paper of how a factor makes the cut. For example, why was Tulip mania not included as a risk factor? Ya never know, it could make a comeback!

A cheeky response to a cheeky comment: Exposure to a Tulip mania comeback is already covered in the Q Risk model by having exposure to the Consumer Cyclical sector (tulips) and the Momentum risk factor (mania). Might do well.

:)

Hi Rene,
I have been away from the Q forum for most of this year and have just seen your post & whitepaper.
This answers a number of questions that were previously unresolved for me. Many thanks indeed to you for your clear exposition.

I now ask for your help with a question that may seem rather pedantic and perhaps has already been dealt with elsewhere, but please bear with me....

In the new format (since I last saw it) version of the Backtester, on the overview page there are displays of Common Returns & Specific Returns. Presumably these correspond exactly to the returns associated with Common Risk and Specific Risk respectively, as now clearly explained in your paper, in which it is stated: "Common risk is defined here as risk attributable to common factors which drive returns within the equity stock market". In the Q Risk Model there are 11 Sector Factors and 5 Style Factors, making a total of 16 (common) factors in all, as used by Q. Although I do not see it stated explicitly, it is therefore my interpretation that the Specific Returns as shown in the backtester are presumably the returns attributable to any causes OTHER than to the 16 factors used in the Q model, i.e. the "Specific Returns" of any given algo are the returns that are NOT explained by any of the existing factors in the Q risk model. Sorry if this sounds rather pedantic, but please can you confirm if I do indeed have this correct?

If so then....
to @Rene and @Delaney
it would be my understanding that presumably:

1) Q actually WANTS the Specific returns to be high as they represent something essentially "new", or at least something that is not captured by the existing Q risk model, right?

2) Presumably Q could potentially add more Style (or other) factors to the risk model. If that was done, then the associated returns would be taken out of "Specific" and placed into "Common" risks & returns. Is that correct?

3) Two seemingly very obvious candidates for additional factors are Liquidity and Quality, neither of which are contained within the existing list of 5 Style Factors but which are often used in stock trading system designs outside of the Q context. Is there any particular reason why these 2 additional factors were NOT included in the list currently in Q's risk model?

4) Is it helpful (either to Q or to the algo author) to openly specify other known or likely factors like this, or is this somehow "giving away the secret sauce"?

Finally a cheeky response to @Joakim: I agree that tulip mania (and other manias) are definitely largely Momentum-driven, but do you think tulips are really consumer CYCLICAL ..... or could there be more to it than that? Think Bitcoin?? ;-))

Cheers, best regards, TonyM.

Hi @Rene,

just read the white paper and got some questions left regarding the methodology.

1. Why are you using cross-sectional regressions in order to calculate the style factor returns? Why don't you just build the style portfolios and use those returns?

2. Using cross-sectional regressions this way, you basically equal weight each stock when calculating the style factor returns. However, the sector ETFs typically value weight their holdings. I don't know if it makes any difference when estimating exposures, however it doesn't seem stringent to me.

3. Why are you using the z-scores of each characteristic? I'm aware that outliers can bias cross-sectional regressions, however most academic papers just use the raw/logarithmised characteristics.

4. How good are those estimates? Recently, I compared several standard methodologies to estimate factor exposures and there are huge discrepancies between estimated/realized exposures.

Best regards,
Michael

A couple of points:
1) The white paper leaves really lacks a lot in the terms of references. Surely there's more than that.
2) There is a fundamental problem in all of these models that there is no test done on the error terms. If the error terms are not iid, (most likely they are not), then the standard errors are incorrect and so are any results from the t-tests.
3) Typically, stock returns have skewness and kurtosis. how does the risk manager handle/alphalens that?