Request for Feedback - Portfolio Optimization API

Suppose you've written an algorithm that's developed a signal (for example, a Pipeline Factor) that's predictive of forward returns over some time horizon. You might think that the hard is over, but you're still left with the daunting task of translating your signal into an algorithm that can turn a profit while also managing risk exposures.

Traditionally in quantitative finance, the solution to the problem of maximizing returns while constraining risk has been to employ some form of Portfolio Optimization, but performing sophisticated optimizations is challenging on today's Quantopian.

Python libraries like scipy.optimize, CVXOPT, and CVXPY (all available on Quantopian today) provide generic tools for solving optimization problems. These libraries are powerful and flexible, but it takes significant expertise to convert the data structures available on Quantopian into the specific formats understood by these libraries.

Algorithm authors who want to perform even simple optimizations spend much of their time to figuring out how to encode conceptually simple ideas like "constrain gross leverage" into complicated matrices that are hard to interpret and hard to debug when something goes wrong.

The open source Python ecosystem already provides excellent implementations of the algorithms needed to implement a quality portfolio optimization library. What's missing is an interface that maps domain concepts from finance onto the low-level primitives exposed by existing libraries.

Today we're announcing quantopian.experimental.optimize, a suite of new tools designed to make portfolio optimization on Quantopian more accessible to algorithm authors. As the name suggests, all the functions and classes currently available are experimental and should be considered subject to change at any time. We are releasing these APIs early in the development cycle so that we can gather feedback about how to make them as useful as possible to the community when the time comes for a stable release.

The optimize module has three major components in this release:

• calculate_optimal_portfolio, a top-level entrypoint.
• Objective classes, representing functions to be minimized or maximized by the optimizer.
• Constraint classes, representing constraints to be enforced by the optimizer.

To run a portfolio optimization, you call calculate_optimal_portfolio and provide three values:

• An Objective to optimize.
• A list of Constraints to enforce.
• A pandas Series containing weights for the current portfolio. The index of the current portfolio series defines the assets that are allowed in the target portfolio.

The attached notebook provides a detailed walkthrough of the new API. There's still a lot of work to do and many questions to answer before we move beyond experimental status. With help from the Quantopian community, we hope to answer these questions in the coming weeks. I'm excited about the opportunities that optimization creates for algorithm authors, and I look forward to hearing feedback and to seeing what the community builds with these tools. The optimization API is only available in notebooks for this release. Work is currently ongoing to make optimization available in the backtester. See the Next Steps section at the bottom of this notebook for more details.

Happy Optimizing!

339
Notebook previews are currently unavailable.
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.

19 responses

Scott -

Just a brief note that one way to approach this is to minimize the difference between the old and new portfolio subject to constraints. For example, your objective function would be the Euclidean distance squared between the old and new portfolio vectors, and the optimization would find the new portfolio, constrained by a minimum forecast profit. It may not be quite the same to maximize the profit, subject to a maximum turnover. If you can actually achieve the minimum forecast profit, then the turnover will be worth it, otherwise do nothing. Having a hard limit on turnover might not make sense, but I'm no expert, for sure.

Scott - great work on this module! It looks to be general enough to accommodate a large number of use cases. A few suggestions on how you would continue to move in forward:

1) Your example uses a an objective with only alpha / expected return. One could also include a covariance term and a transaction cost term. Ron Kahn's book (https://www.amazon.com/Active-Portfolio-Management-Quantitative-Controlling/dp/0070248826) includes some basic models on estimating transaction costs using volume and volatility, and there are other more involved models. Maybe the most important thing to keep in mind with the covariance is that the matrix is not ill-conditioned, which can happen with large numbers of assets, and estimation errors will propagate throughout the optimization.

2) Constrain exposures to factors in addition to sectors. While one may not want to have too much exposure to a given sector, one may also want to hedge out exposures to common risk factors (market, value, size, momentum, liquidity) so that the resulting portfolio is loading only on the "alpha."

3) Does the optimization engine work on non-convex problems? CVXOPT and CVXPY are great for convex problems, and scipy.optimize.minimize can work on non-convex problems. Does the module recognize automatically if the problem is convex?

It seems that MaxGrossLeverage should be an equality constraint, but you imply it is an inequality. Don't we really want the gross leverage to be equal to 1.0, versus in the range -Inf to 1.0, inclusive?

Regarding PositionConcentration is this really a constraint in the optimization, or bounds? I think there is a distinction between adding a set of inequality constraints, and limiting the search space (bounds). Are you adding constraints, or limiting the search space?

Is the idea that this would be run within the 5 minutes of before_trading_start? Or within the 50 seconds of handle_data or a scheduled function? Or something else? One problem I see is that the optimization needs to be time-bound. If a solution is not found within a time frame, then a flag needs to be returned so that the algo decide what to do.

Python libraries like scipy.optimize, CVXOPT, and CVXPY (all available on Quantopian today) provide generic tools for solving optimization problems.

I didn't realize that CVXPY is now available. It has been in the queue for awhile now ( https://www.quantopian.com/posts/request-for-cvxpy-in-quantopian ). Is it now working in research & the backtester?

Also, there was an open issue with CVXOPT ( https://www.quantopian.com/posts/help-w-slash-cvxopt )? Did it get addressed?

You might elaborate on how the portfolio turnover constraint/minimization would account for changes in the universe? I can see how things might work for a fixed set of stocks (an incremental rotation of the portfolio vector in its N-dimensional space), but if you are doing adds/drops, it is not so clear.

Ryan -

Your example uses an objective with only alpha / expected return. One could also include a covariance term and a transaction cost term.

Yep, totally agree! I tried to keep the examples/implementations simple for the first cut, but both of these additions are in the roadmap. There are a couple ways I could see these playing out, API-wise:

1. MaximizeAlpha gains some new arguments (e.g. covariance_matrix, and risk_aversion), and uses them to build a more complex objective internally. The major cost here is that MaximizeAlpha's signature ends up pretty bloated and will be harder to understand for newcomers.
2. We keep the existing simple MaximizeAlpha and add more complex objectives like MaximizeRiskAdjustedAlphaMinusTransactionCost. The benefit here would be that only users who want the additional complexity are forced to deal with it. The cost is that the more complex version's name is a mouthful, and it's still intimidating for users looking to move from a simpler model to a more complex one.
3. We add a new kind of expression, Penalty, to go along with Constraint and Objective. This has the benefit of modularizing the independent components of the optimization function, but has the cost that it's less clear which penalties are appropriate for which objectives. Minimizing expected covariance, for example, doesn't really make sense when using TargetPortfolio as an objective.

Constrain exposures to factors in addition to sectors.

I think you can do that today with the WeightedExposure constraint, which takes an assets x labels DataFrame of weighting coefficients and min/max exposure vectors for each label. I left WeightedExposure out of the examples because it's hard to motivate in a short paragraph, but I probably should have at least added a mention somewhere.

Does the optimization engine work on non-convex problems?

It only works on convex problems. Constraints and Objectives are essentially factories for CVXPY expressions that handle input validation and data alignment. We're mostly deferring to CVXPY (and by extension, CVXOPT, ECOS, and other solvers) for the heavy lifting here.

Grant -

one way to approach this is to minimize the difference between the old and new portfolio subject to constraints.

I think this is precisely the behavior of the TargetPortfolio objective I outline in the second notebook example. Am I missing something? Regarding your point of trying to do the optimization and failing if we can't achieve a target return, assuming we had something like a MinimumReturn constraint, you could do something like:

objective = TargetPortfolio(...)
constraints = [MinimumReturn(expected_returns, MINIMUM)]
try:
calculate_optimal_weights(objective, constraints, current_portfolio)
except opt.InfeasibleConstraints:
pass # Couldn't improve expected return enough to be worthwhile.


In practice, I think people tend to handle this problem by applying a transaction cost penalty, or by simply ignoring the results of the optimization if they don't change your expected return enough to be worthwhile.

It seems that MaxGrossLeverage should be an equality constraint, but you imply it is an inequality. Don't we really want the gross leverage to be equal to 1.0, versus in the range -Inf to 1.0, inclusive?

Gross leverage is the sum of the absolute values of your positions, so it's greater than or equal to 0 by definition. MaxGrossLeverage(1.0) constrains gross leverage to be less than or equal to 1.0, inclusive, which means that the sum of the magnitudes of our longs and shorts has to be less than or equal to the liquidation value of our portfolio. This prevents me, for example, from going long 10 million dollars in stock X and short 10 million dollars in stock Y unless I have a capital base of at least 20 million, but it doesn't by itself prevent me from having an empty portfolio. Objectives like MaximizeAlpha, however, will naturally push position weights up until they run into constraints. If you're using MaximizeAlpha with MaxGrossLeverage, you'll likely end up with a leverage equal to your max unless you have other constraints that prevent that from happening.

I think there is a distinction between adding a set of inequality constraints, and limiting the search space (bounds).

Supplying an inequality (or equality) constraint in this context is the same as limiting the search space in this context.

One good way to build intuition for how this style of optimization is to take a geometric perspective on the problem. The variable under optimization is a length-N vector of stock weights, so we can imagine our optimization problem as a search in N-dimensional space. The objective defines a potential function on our space that we either want to minimize or maximize, and the constraints describe a region of space that we'll allow as valid solutions. Mathematically, individual constraints are expressed as vector inequalities in N-space. Geometrically, solutions to those inequalities describe regions of N-space, and applying multiple constraints corresponds to taking intersections of those regions. Specifically, for the class of constraints supported here, the regions of space are required to be convex, which means they look like planes, paraboloids, ellipsoids, or similar nicely-behaved surfaces. Constraining ourselves to convex regions makes the problem of computing solutions considerably more tractable.

Or within the 50 seconds of handle_data or a scheduled function?

Likely within the 50 seconds. So far I haven't run any realistic optimizations that have taken longer than 30 seconds or so on a reasonably-powerful laptop.

I didn't realize that CVXPY is now available. Is it now working in research & the backtester?

Yep. It was whitelisted as part of this work. I may make a separate announcement post at some point.

Also, there was an open issue with CVXOPT

Your CVXOPT example looks like it works now for me. I'm not sure when that was fixed. My guess would be that it was fixed during the Q2 refactor.

Thanks Scott -

Yes, I see now that the TargetPortfolio objective is the Euclidean distance. Doesn't it need to be the square of the Euclidean distance for the problem to be convex?

Section 4.2 of this paper discusses a relevant optimization problem:

http://icml.cc/2012/papers/168.pdf

The forecast return is expressed as an inequality constraint. If it is not met, then there is no update to the portfolio. Rather than try-except, you probably need a success-fail flag (such res in scipy.optimize.minimize).

Regarding MaxGrossLeverage(1.0), it seems like there might also be a use case of GrossLeverage(1.0) where my constraint is to be 100% in the market at all times.

How does this optimization suite handle changes in the portfolio vector, when comparing old to new? For example, the TargetPortfolio objective (I think) requires that the two portfolio vectors be the same length and correspond to the same stocks. Similar considerations apply when limiting turnover. There needs to be a provision in the optimization to handle stock add/drop events, right?

I suggest supporting implementation both in before_trading_start and handle_data and scheduled functions. If you look in the paper I reference above, Section 4.3, the author describes a technique for averaging over an expanding look-back window. It amounts to running the optimization over each window to achieve a smoothing/averaging effect of the portfolio vector versus time. So, your 30 seconds would be multiplied times ~ N/2, the number of look-back windows. So, for N = 20, it'll take 5 minutes. Of course, it'd be nice to do this sort of thing with parallel processing, but we ain't there yet, I gather.

So far I haven't run any realistic optimizations that have taken longer than 30 seconds or so on a reasonably-powerful laptop.

How is your laptop performance relevant? Aren't you doing this work on the Q research platform?

Scott -

Regarding when to run the optimization, if I follow your overall workflow concept, the time scale for the alphas would be daily/weekly/monthly/quarterly (both because pipeline only supports daily OHLCV bars for computing returns, and because fundamentals (and most other data sets?) are on relatively long time scales). So, running the optimization within the trading day doesn't really make sense in my mind. We aren't looking to do minute-by-minute, or even day-by-day trading, in most cases. There is no advantage in doing optimizations during the trading day, particularly if they are time-bound to 50 seconds. The same would apply to alpha combination (ML or whatever), which is another kind of optimization step, prior to your portfolio optimization.

For the Q fund, it would seem that you just need a set of buy/sell signals before the market open, and then you can sort out how to execute the combined signals over the trading day in an optimal way.

Yes, I see now that the TargetPortfolio objective is the Euclidean distance. Doesn't it need to be the square of the Euclidean distance for the problem to be convex?

The function "Euclidean norm from a point X" is convex without requiring any further transformation. One way of thinking about this is to notice that inequality constraints against this function describe (hyper) spheres centered around X. The CVXPY docs provide an excellent taxonomy of functions commonly used in optimization and descriptions of what it means to be convex and concave

How does this optimization suite handle changes in the portfolio vector, when comparing old to new? For example, the TargetPortfolio objective (I think) requires that the two portfolio vectors be the same length and correspond to the same stocks.

The optimization uses the third argument (current_portfolio) to calculate_target_weights as its universe definition, expecting entries with weights of 0.0 for stocks not currently held but under consideration. If a constraint/objective doesn't know about a stock in the portfolio vector, it generally does the most conservative thing possible w/r/t the unknown assets. The exact behavior here depends on the particular constraint or objective. TargetPortfolio and MaximizeAlpha, for example, assume that any stocks not provided should be given a target weight/expected return of 0. NetPartitionExposure applies constraints forcing assets with unknown partition labels to have positions of size 0.PositionConcentration takes default_min_weight and default_max_weight arguments that are used when an unknown stock is encountered in the portfolio.

Regarding when to run the optimization, if I follow your overall workflow concept, the time scale for the alphas would be daily/weekly/monthly/quarterly (both because pipeline only supports daily OHLCV bars for computing returns, and because fundamentals (and most other data sets?) are on relatively long time scales). So, running the optimization within the trading day doesn't really make sense in my mind.

I think this is generally correct, though I could still imagine algorithms where a portion of your alpha or risk analysis depended on minute-scale pricing data. In particular, I could imagine using minute-scale price data to build a covariance matrix, which is often an input term in more sophisticated optimizations. Regardless, there will certainly be algorithms that only depend on daily-or-lower frequency data, and for those algorithms, optimizing in before_trading_start makes sense.

Thanks Scott -

Regarding running the optimization within before_trading_start, one issue is that I don't think functions can be scheduled there, right? This would be kind of inconvenient, since then one would either have to run the optimization every day, which may not be required, or kludge some sort of scheduling. Other than not being able to submit orders and schedule functions, are there other limitations to before_trading_start?

I could still imagine algorithms where a portion of your alpha or risk analysis depended on minute-scale pricing data

Those data would be available via history in before_trading_start, correct?

A more general architectural question is how the output of the optimization will interface with the order/execution management system mentioned by Fawce on https://www.quantopian.com/posts/big-news-for-the-quantopian-community-managing-external-capital :

The selected algorithms will connect to a prime broker through an order/execution management system (O/EMS) at Quantopian.

I'd think that unless an algo is justifiably relatively short-term intraday, it'd be advantageous to have all of the allocations for the day ready before the market opens. Otherwise, you'll have the orders hitting willy nilly throughout the day, and the O/EMS system won't be able to manage them as a whole on a daily basis (which would seem to be the relevant time scale). It seems that you'd want the orders to be queued and ready before the market opens, no?

Hi Scott -

One concept you might consider is that you are rotating the portfolio vector in its N-dimensional space. For example, if N = 2, then I have r = (x,y), and if I require both long and short at equal weight, then I can have r = (1,-1)/sqrt(2) or r = (-1,1)/sqrt(2), so the only allowed angles of rotation are +/- pi. When I make an allocation change, my portfolio vector sweeps out a semi-circle. Analogously, for N > 2, one could constrain the maximum angle step and the path. For N = 3, one would rotate the vector along a great circle route, with each step no more than delta_theta radians (i.e. more than one trade may be required to complete the portfolio vector rotation).

For a large long-short portfolio, conceptually, I'd think one would want certain characteristics of the portfolio vector path in the N-dimensional space.

Grant

Hi Scott -

Is CVXPY now supported in the backtester? Pravin is reporting he's getting an error (see https://www.quantopian.com/posts/anyone-successfully-used-cvxpy ). I'd like to code an example algo and post it.

Thanks,

Grant

Scott -

You may want to consider embedding some sort of expanding window/bootstrapping in the portfolio optimization toolkit. Basically, to compute the new portfolio vector, you'd run N optimizations, and average them (perhaps with weights). The issue, as I understand it, is that a single optimization will tend to yield a portfolio vector that jerks around a lot. The expanding window approach in the OLMAR paper may do the trick (which weights by the expected return for each window). A potential modification would be to add a weight that favors shorter windows over longer ones.

Of course, users could do this jazz on their own, but it begs for parallelization, which you could prep for under the hood, if you include this approach in the toolkit. Even if not done in parallel, you could probably do a better job of optimizing the code than users might.

Hi Scott -

1. If I understand correctly, the optimization would be based on a look-back of a single period. Using the current portfolio weight vector, w(-1), a new portfolio weight vector, w(0), would be computed. However, the optimization could incorporate a longer history of portfolio weight vectors, [w(-1), w(-2),...w(-N)]. So, in addition to getting the current portfolio state, w(-1), across the universe, you'd have to store a portfolio history. It seems like the history would be useful, for smoothing the trajectory of the portfolio.
2. If I'm interpreting Thomas W.'s comments correctly, the initial release of the workflow through the alpha combination step will be pipeline-based, using pipeline factors and daily bars. Will the optimization be within pipeline, as well? I'm imagining that prior to the market open (e.g. in before_trading_start), the workflow would run, with the exception of the Execution step. So, before the market opens, the new portfolio weight vector, w(0), would be available. Or do you see an advantage in running the optimization during the trading day?

When cloning and running the NB I get:


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-10-50a696070b13> in <module>()
----> 1 constrained_leverage_pf = optimal_portfolio_constrained_leverage_only()
2 draw_asset_barplot(constrained_leverage_pf, 'Optimal Portfolio (Constrained Leverage Only)');

<ipython-input-9-b1d06a3b7b78> in optimal_portfolio_constrained_leverage_only()
8     constraints = [opt.MaxGrossLeverage(1.0)]
9
---> 10     return opt.calculate_optimal_portfolio(objective, constraints, empty_portfolio)

/build/src/qexec_repo/qexec/optimize/api.py in calculate_optimal_portfolio(objective, constraints, current_portfolio)
68                                     constraints,
69                                     current_portfolio):
---> 70         return optimizer.calculate_new_weights(
71             objective,
72             constraints,

AttributeError: 'str' object has no attribute 'calculate_new_weights'


from this cell:
 constrained_leverage_pf = optimal_portfolio_constrained_leverage_only() draw_asset_barplot(constrained_leverage_pf, 'Optimal Portfolio (Constrained Leverage Only)'); 

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 Thomas,

That error you're seeing is a bug in the most recent research env release. Will fix shortly.

Grant, thanks for all the feedback. I don't always have time to write up a detailed reply, but I do still read and think about the comments here. I expect to be posting a new update on this API soon.

Hi Scott -

Following up on one of my comments above, my understanding at this point is that although the workflow is general with respect to its timescale ("...the cycle time could be once a week, once a day, every 15 minutes, etc."), the initial release will be geared toward pipeline-based factors, with the alpha combination module also embedded within pipeline (i.e. not accessible within the trading day). So, the question comes to mind if there would be any advantage in running the portfolio construction step within the trading day, or if it should immediately follow the alpha combination step (e.g. in before_trading_start)? If you run the portfolio construction (optimization) before the open, then your estimate for the current portfolio will be stale; presumably, waiting ~30 minutes into the trading day would provide a much better estimate of the current portfolio (but might miss out on some return, if the timescale of the forecasting is daily). However, doing the optimization within the trading day then opens up the question of when to do it, and how will it interact with the order/execution management system (as Fawce mentions, "The selected algorithms will connect to a prime broker through an order/execution management system (O/EMS) at Quantopian")?

This opens up a more general discussion of how users wanting to write algos per the initial release of the workflow should think about order types and submission. If I follow the game plan here (assuming the portfolio construction is run before the open), you'll probably just want a normalized portfolio vector prior to the open, and you'll take it from there (i.e. users won't actually define orders within their algos). So, will there be a proxy for the O/EMS system that users can apply for backtesting and paper trading? It seems like a standardized approach would be in order here, or you'll have all kinds of willy-nilly order types and intra-day ordering jazz that won't fit with the initial release of the workflow, that you'll have to unravel and interpret (without access to the logic of the strategy). What is the plan?

Hi Scott,

I'm trying:

objective = opt.TargetPortfolio(target_weights=actual_weight)

and getting an error:

AttributeError: 'module' object has no attribute 'TargetPortfolio'
There was a runtime error on line 112.

Am I doing something wrong, or was there a change?

Hi Scott, Great job on the optimize module. Do you have an open source implementation of the order_optimal_portfolio and the objective functions to understand what's going on behind the scenes?

Thanks