Base finite allocation: private API

PortfolioOptimisers.FiniteAllocationOptimisationEstimatorType
abstract type FiniteAllocationOptimisationEstimator <: OptimisationEstimator

Abstract supertype for finite allocation portfolio optimisation estimators.

Finite allocation estimators convert continuous portfolio weights into discrete share quantities given an investment budget and asset prices.

The library ships two: DiscreteAllocation, which solves a mixed-integer programme, and GreedyAllocation, which walks the target weights. Both take a FiniteAllocationInput as the second argument to optimise, and both split the portfolio into a long and a short sub-problem.

Related

References

  • [145] R. A. Martin. PyPortfolioOpt: portfolio optimization in Python. Journal of Open Source Software 6, 3066 (2021).
source
PortfolioOptimisers.allocation_horizonFunction
allocation_horizon(res::NonFiniteAllocationOptimisationResult,
                   horizon::Option{<:Number} = nothing)

Read the horizon a finite allocation charges its rates over, from an optimisation result.

A stated horizon wins. Otherwise the horizon is the observation count of the result's prior, which is the period count calc_total_fees charges l, s and tn over on the window the fit saw. A result exposing no prior, or one whose prior is nothing, derives none and answers nothing; FiniteAllocationInput then asks the caller for a horizon, and only when the input also carries a fee.

The property is read through hasproperty rather than by dispatch, exactly as extract_fees reads fees: the families that keep a prior keep it under the same name, and the ones that keep none are the point of the fallback.

Arguments

  • res: Fitted optimisation result, potentially carrying a pr property.
  • horizon: Horizon stated by the caller, which wins when it is not nothing.

Returns

  • horizon::Option{<:Number}: The horizon in periods, or nothing when none can be derived.

Related

source
PortfolioOptimisers.setup_alloc_optimFunction
setup_alloc_optim(w::VecNum, cash::Number)

Split a portfolio into its long and its short side, and share the cash between them.

Both finite allocators solve one sub-problem per side. This routine computes the budget of each side, and gives each side the share of the cash its budget calls for.

The routine charges no fee. A fee is a cost of the portfolio the allocator actually buys, so each sub-problem charges its own side inside its own model, on the money it buys. See set_allocation_fees!.

Arguments

  • w::VecNum: Target portfolio weights over the whole universe.
  • cash::Number: Cash available.

Returns

  • bgt::Number: Total budget, sum(w).
  • lbgt::Number: Long-side budget, the sum of the non-negative weights.
  • sbgt::Number: Short-side budget, the negated sum of the negative weights, so it is non-negative.
  • lidx: Mask of the long side, w .>= 0.
  • sidx: Mask of the short side. Empty when the portfolio is long only.
  • lcash::Number: cash * lbgt, before adjust_long_cash corrects it.
  • scash::Number: cash * sbgt. Zero when the portfolio is long only.

Details

  • A zero weight counts as long, because the test is w .>= 0.
  • lcash is the long side's share of the cash. It is only correct once the short side has reported what it did not spend, which is why adjust_long_cash runs between the two sub-problems.

Related

source
PortfolioOptimisers.allocation_turnover_moneyFunction
allocation_turnover_money(::Nothing, ::Number, ::Any, ::Bool)
allocation_turnover_money(tn::Turnover, prev_cash::Number, idx, short::Bool)

Give one side its turnover rate and the money it held per asset before the trade.

tn.w is a weight vector, so the money held per asset before the trade is prev_cash * tn.w. A short side is allocated with its weights negated, so its money is negated too, and both sides then hold a non-negative figure.

Arguments

  • tn: The turnover of the fee, or nothing when the fee states none.
  • prev_cash::Number: Cash held in the portfolio before the trade.
  • idx: Mask of this side.
  • short::Bool: Whether this side is the short one.

Returns

  • val: The turnover rate of this side, or nothing.
  • prev_money: The money held per asset before the trade, or nothing.

Related

source
PortfolioOptimisers.allocation_liquidation_feeFunction
allocation_liquidation_fee(::Nothing, ::Number, ::Number)
allocation_liquidation_fee(fees::Fees, T::Number, prev_cash::Number)

Charge the forced exit of every asset that left the universe, as one constant.

A Fees carries two liquidation carriers. lq is a rate and flq is a currency amount, and both key on the previous weight of a position the optimisation was forced to sell. The exiting assets are not among the share counts an allocator solves for, so neither carrier needs a variable and the whole charge is a constant of the sub-problem — which is what set_liquidation_fees! and set_fixed_liquidation_fees! found for the JuMP model.

The charge is in money, as every term of an allocation must be. lq.w is a weight, so the money the exit sold is prev_cash * lq.w, the rule allocation_turnover_money reads for tn. flq is already a currency amount, so it is charged once for each entry whose previous weight is not isapprox to zero, on both sides of the book, exactly as calc_fixed_liquidation_fees charges it.

The whole charge falls on the long sub-problem. Every exiting asset carries a zero target weight, and setup_alloc_optim puts a zero weight on the long side, so that side is the one the allocator's own split gives them. It is a constant, so the split moves no reported number; it decides which budget pays, and a long-only book has no other.

Algorithm

  1. On a nothing fees, return a zero. Both carriers are nothing, so nothing left the universe.
  2. Per period: contract lq.val with abs.(prev_cash * lq.w), and multiply by T.
  3. One time: charge flq through calc_fixed_liquidation_fees, which sums the liquidated long and the liquidated short side.
  4. Return the sum of the two.

Arguments

  • fees: The fee carrying the two liquidation carriers, or nothing.
  • T::Number: Horizon, in periods.
  • prev_cash::Number: Cash held in the portfolio before the trade.

Returns

  • fee::Number: The whole forced-exit charge over the horizon.

Related

source
PortfolioOptimisers.allocation_side_feesFunction
allocation_side_fees(::Nothing, ::Option{<:Number}, ::Number, ::Any, ::Any)
allocation_side_fees(fees::Fees, T::Number, prev_cash::Number, lidx, sidx)

Split a fee into the long side's charge and the short side's charge.

Each sub-problem charges its own side. The long side takes l and fl, the short side takes s and fs, and both take the turnover rate and the money they held before the trade. A rate that is a vector is viewed to the side, and a rate that is a scalar is carried through, which is what nothing_scalar_array_view does.

lidx and sidx are masks of the full universe, because they are derived from the weights an optimisation expanded back to it. A fee that same optimisation reduced to its Investable Mask is on a shorter axis, and carries that mask in imsk, so this verb lifts it with lift_fees before it takes a single view. An unmarked fee lifts nothing, which is the fee a caller wrote by hand and the fee of an optimisation that reduced on nothing.

The forced exit of allocation_liquidation_fee is a constant of the whole allocation rather than of one side, and it rides on the long side's charge as liq. The short side carries a zero in that slot, so both charges read alike.

Arguments

  • fees: The fee to split, or nothing when the caller states none.
  • T: Horizon, in periods.
  • prev_cash::Number: Cash held in the portfolio before the trade.
  • lidx: Mask of the long side.
  • sidx: Mask of the short side.

Returns

  • lsf: The long side's charge, or nothing.
  • ssf: The short side's charge, or nothing.

Each charge is a named tuple of T, prop, fixed, tn_val, prev_money and liq.

Related

source
PortfolioOptimisers.permute_side_feesFunction
permute_side_fees(::Nothing, ::Any)
permute_side_fees(sf::NamedTuple, idx)

Put one side's charge into the order idx names.

finite_sub_allocation! sorts its assets by descending target weight, so the rates and the previous money must take the same order. A rate that is a vector is viewed to idx, and a rate that is a scalar is carried through, which is what nothing_scalar_array_view does. liq is a constant of the whole side rather than a per asset rate, so it carries through whatever the order is.

Arguments

Returns

  • sf: The charge in the order idx names, or nothing.

Related

source
PortfolioOptimisers.allocation_feeFunction
allocation_fee(::Nothing, ::VecNum, shares::VecNum)
allocation_fee(sf::NamedTuple, p::VecNum, shares::VecNum)

Charge one side's whole fee against a share vector.

shares .* p is the money in each position exactly, and every term is charged against that money. No weight and no price appears on its own. This is the rule every term of an allocation follows, and it is the number both allocators report.

sf is one side's charge, of allocation_side_fees. A nothing sf charges nothing.

Algorithm

  1. Proportional, per period: the rate contracted with the money.
  2. Turnover, per period: the rate contracted with the money traded, |money - prev_money|.
  3. Fixed, one time: the rate contracted with the indicator of a non-zero position.
  4. Forced exit, sf.liq: a constant of allocation_liquidation_fee, already over the horizon.
  5. Return T times the first two terms, plus the third and the fourth.

A side that held money before the trade owes a turnover fee even when it buys nothing, because selling out is a trade, and a book whose universe lost an asset owes the forced exit whatever it buys. That is why both allocators charge this verb against an empty book.

Arguments

  • sf: One side's charge, of allocation_side_fees, or nothing.
  • p::VecNum: Asset prices of this side.
  • shares::VecNum: Share count per asset.

Returns

  • fee::Number: The whole charge of this side over the horizon.

Related

source
PortfolioOptimisers.adjust_long_cashFunction
adjust_long_cash(bgt::Number, lcash::Number, scash::Number) -> Number

Correct the long side's cash with the cash the short side did not spend.

Runs between the two sub-problems, once the short side has reported its leftover cash. The correction has opposite signs above and below a unit budget, so the long side never spends cash the portfolio does not hold.

Arguments

  • bgt::Number: Total budget, sum(w).
  • lcash::Number: Long side's share of the gross cash, from setup_alloc_optim.
  • scash::Number: Cash the short side did not spend.

Returns

  • res::Number: The corrected long-side cash.

Details

  • scash == 0: lcash is returned unchanged. A long-only portfolio takes this branch.
  • bgt >= 1: lcash exceeds the cash actually available, so the unspent short cash is subtracted. It is not available to the long side.
  • bgt < 1: lcash falls short of the cash actually available, so the unspent short cash is added without exceeding the true budget.

Related

source

References

[145]
R. A. Martin. PyPortfolioOpt: portfolio optimization in Python. Journal of Open Source Software 6, 3066 (2021).