Postprocessing weights¶
After optimal weights have been generated, it is often necessary to do some postprocessing 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 longonly and shortonly portions. If your portfolio has shorts,
you should pass a short ratio. The default is 0.30, corresponding to a 130/30 longshort 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
 weights (dict) – continuous weights generated from the

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