Post-processing weights

After optimal weights have been generated, it is often necessary to do some post-processing before they can be used practically. In particular, you are likely using portfolio optimisation techniques to generate a portfolio allocation – a list of tickers and corresponding integer quantities that you could go and purchase at a broker.

However, it is not trivial to convert the continuous weights (output by any of our optimisation methods) into an actionable allocation. For example, let us say that we have $10,000 that we would like to allocate. If we multiply the weights by this total portfolio value, the result will be dollar amounts of each asset. So if the optimal weight for Apple is 0.15, we need $1500 worth of Apple stock. However, Apple shares come in discrete units ($190 at the time of writing), so we will not be able to buy exactly $1500 of stock. The best we can do is to buy the number of shares that gets us closest to the desired dollar value.

PyPortfolioOpt offers two ways of solving this problem: one using a simple greedy algorithm, the other using integer programming.

Greedy algorithm

DiscreteAllocation.greedy_portfolio() proceeds in two ‘rounds’. In the first round, we buy as many shares as we can for each asset without going over the desired weight. In the Apple example, \(1500/190 \approx 7.89\), so we buy 7 shares at a cost of $1330. After iterating through all of the assets, we will have a lot of money left over (since we always rounded down).

In the second round, we calculate how far the current weights deviate from the existing weights for each asset. We wanted Apple to form 15% of the portfolio (with total value $10,000), but we only bought $1330 worth of Apple stock, so there is a deviation of \(0.15 - 0.133\). Some assets will have a higher deviation from the ideal, so we will purchase shares of these first. We then repeat the process, always buying shares of the asset whose current weight is furthest away from the ideal weight. Though this algorithm will not guarantee the optimal solution, I have found that it allows us to generate discrete allocations with very little money left over (e.g $12 left on a $10,000 portfolio).

That being said, we can see that on the test dataset (for a standard max_sharpe portfolio), the allocation method may deviate rather widely from the desired weights, particuarly for companies with a high share price (e.g AMZN).

Funds remaining: 12.15
MA: allocated 0.242, desired 0.246
FB: allocated 0.200, desired 0.199
PFE: allocated 0.183, desired 0.184
BABA: allocated 0.088, desired 0.096
AAPL: allocated 0.086, desired 0.092
AMZN: allocated 0.000, desired 0.072
BBY: allocated 0.064, desired 0.061
SBUX: allocated 0.036, desired 0.038
GOOG: allocated 0.102, desired 0.013
Allocation has RMSE: 0.038

Integer programming

This method (credit to Dingyuan Wang for the implementation) treats the discrete allocation as an integer programming problem. In effect, the integer programming approach searches the space of possible allocations to find the one that is closest to our desired weights.

Unfortunately, scipy does not support integer programming so we must instead use the PuLP library. I’m not a huge fan of their API, for example using opt += a <= b to add a constraint to the optimisation problem, but overall it is a simple library to get up and running with.

Caution

Though lp_portfolio() produces allocations with a lower RMSE, some testing shows that it is between 100 and 1000 times slower than greedy_portfolio(). This doesn’t matter for small portfolios (it should still take less than a second), but the runtime for integer programs grows exponentially as the number of stocks, so for large portfolios you may have to use greedy_portfolio().

Dealing with shorts

As of v0.4, DiscreteAllocation automatically deals with shorts by finding separate discrete allocations for the long-only and short-only portions. If your portfolio has shorts, you should pass a short ratio. The default is 0.30, corresponding to a 130/30 long-short balance. Practically, this means that you would go long $10,000 of some stocks, short $3000 of some other stocks, then use the proceeds from the shorts to go long another $3000. Thus the total value of the resulting portfolio would be $13,000.

Usage

The discrete_allocation module contains the DiscreteAllocation class, which offers multile methods to generate a discrete portfolio allocation from continuous weights.

class pypfopt.discrete_allocation.DiscreteAllocation(weights, latest_prices, min_allocation=0.01, total_portfolio_value=10000, short_ratio=0.3)

Generate a discrete portfolio allocation from continuous weights

Instance variables:

  • Inputs:

    • weights
    • latest_prices
    • min_allocation
    • total_portfolio_value
    • short_ratio
  • Output: allocation

Public methods:

  • greedy_portfolio()
  • lp_portfolio()
__init__(weights, latest_prices, min_allocation=0.01, total_portfolio_value=10000, short_ratio=0.3)
Parameters:
  • weights (dict) – continuous weights generated from the efficient_frontier module
  • latest_prices (pd.Series or dict) – the most recent price for each asset
  • min_allocation (float, optional) – any weights less than this number are considered negligible, defaults to 0.01
  • total_portfolio_value (int/float, optional) – the desired total value of the portfolio, defaults to 10000
  • short_ratio (float) – the short ratio, e.g 0.3 corresponds to 130/30
Raises:
  • TypeError – if weights is not a dict
  • TypeError – if latest_prices isn’t a series
  • ValueError – if not 0 < min_allocation < 0.3
  • ValueError – if short_ratio < 0
_allocation_rmse_error(verbose=True)

Utility function to calculate and print RMSE error between discretised weights and continuous weights. RMSE was usen instead of MAE because we want to penalise large variations.

Parameters:verbose (bool) – print weight discrepancies?
Returns:rmse error
Return type:float
greedy_portfolio(verbose=False)

Convert continuous weights into a discrete portfolio allocation using a greedy iterative approach.

Parameters:verbose (bool) – print error analysis?
Returns:the number of shares of each ticker that should be purchased, along with the amount of funds leftover.
Return type:(dict, float)
lp_portfolio(verbose=False)

Convert continuous weights into a discrete portfolio allocation using integer programming.

Parameters:verbose (bool) – print error analysis?
Returns:the number of shares of each ticker that should be purchased, along with the amount of funds leftover.
Return type:(dict, float)