The source files can be found in examples/.

Finite allocation

An optimiser returns continuous weights, which are fractions of capital. To trade them you buy whole shares at market prices with a fixed amount of cash. The rounding moves the portfolio you hold away from the target weights, and the smaller the account, the larger the move. PortfolioOptimisers.jl has two finite-allocation optimisers, and you call both as optimise(allocator, FiniteAllocationInput(; w, prices, cash)).

  • GreedyAllocation rounds each weight to whole shares, or to lots, and then spends the cash left over on the assets furthest below their target. It needs no solver.
  • DiscreteAllocation solves a mixed-integer programme. It chooses the share counts that minimise the sum of the absolute differences, in money, between the target and the bought positions, plus the cash left over. It needs a solver for mixed-integer programmes.
When to reach for this

Reach for a finite allocation as the last step before you trade, because you buy shares, not fractions of capital. Use GreedyAllocation when you want an answer at once, and for a large portfolio where the mixed-integer programme is slow. Use DiscreteAllocation when the account is small enough that the rounding matters and you want the share counts that the mixed-integer programme finds. The sum of the absolute differences between the weights you hold and the target weights is the rounding error, and each table below prints it.

using PortfolioOptimisers, CSV, TimeSeries, DataFrames, PrettyTables, Clarabel, HiGHS,      StatsPlots, GraphRecipesresfmt = (v, i, j) -> begin    return if j == 1        v    else        isa(v, AbstractFloat) ? "$(round(v*100, digits=3)) %" : v    endend;cashfmt = (v, i, j) -> begin    return if j == 2        "\```math(round(v, digits = 2))"    else        resfmt(v, i, j)    endend;

1. A target portfolio and prices

A finite allocation needs the target weights and a price per share. We optimise a minimum-risk portfolio for the weights and take the latest prices from the price table.

X = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)pr = prior(EmpiricalPrior(), rd)slv = Solver(; name = :clarabel, solver = Clarabel.Optimizer,             settings = Dict("verbose" => false),             check_sol = (; allow_local = true, allow_almost = true))res = optimise(MeanRisk(; obj = MinimumRisk(), opt = JuMPOptimiser(; pe = pr, slv = slv)))prices = vec(values(X)[end, :])
20-element Vector{Float64}:
 125.674
  62.57
  32.301
  78.279
 173.728
  63.883
 311.22
 174.085
 129.575
  62.609
 363.098
 109.581
 233.434
 179.278
  49.25
 149.133
  24.497
 524.422
 140.181
 106.627

2. Greedy allocation

GreedyAllocation needs no solver. Its result holds the whole number of shares per asset, the cost per asset, the weights w of the shares bought, and the cash left over. We allocate a budget of $100,000. The function rounding_error sums the absolute differences between the weights bought and the target weights, and the title of the table prints it with the cash left over.

cash = 100_000.0greedy = optimise(GreedyAllocation(),                  FiniteAllocationInput(; w = res.w, prices = prices, cash = cash))rounding_error(alloc) = sum(abs, alloc.w .- res.w)pretty_table(DataFrame("Asset" => rd.nx, "Target" => res.w,                       "Shares" => round.(Int, greedy.shares), "Realised" => greedy.w);             formatters = [resfmt],             title = "Greedy allocation of \```(round(Int, cash)): leftover cash \$$(round(greedy.cash, digits = 2)), rounding error $(round(rounding_error(greedy), digits = 4))")
Greedy allocation of ```(round(Int, cash)): leftover cash $14.22, rounding error 0.0033
┌────────┬──────────┬────────┬──────────┐
│  Asset │   Target │ Shares │ Realised │
│ String │  Float64 │  Int64 │  Float64 │
├────────┼──────────┼────────┼──────────┤
│   AAPL │    0.0 % │      0 │    0.0 % │
│    AMD │    0.0 % │      0 │    0.0 % │
│    BAC │    0.0 % │      0 │    0.0 % │
│    BBY │    0.0 % │      0 │    0.0 % │
│    CVX │  7.432 % │     43 │  7.471 % │
│     GE │  0.806 % │     13 │  0.831 % │
│     HD │    0.0 % │      0 │    0.0 % │
│    JNJ │ 36.974 % │    212 │ 36.911 % │
│    JPM │  0.749 % │      6 │  0.778 % │
│     KO │ 11.161 % │    178 │ 11.146 % │
│    LLY │    0.0 % │      0 │    0.0 % │
│    MRK │ 17.467 % │    159 │ 17.426 % │
│   MSFT │    0.0 % │      0 │    0.0 % │
│    PEP │  8.978 % │     50 │  8.965 % │
│    PFE │    0.0 % │      0 │    0.0 % │
│     PG │  2.353 % │     16 │  2.386 % │
│    RRC │    0.0 % │      0 │    0.0 % │
│    UNH │    0.0 % │      0 │    0.0 % │
│    WMT │  9.355 % │     67 │  9.393 % │
│    XOM │  4.725 % │     44 │  4.692 % │
└────────┴──────────┴────────┴──────────┘

3. Exact allocation with a mixed-integer solver

DiscreteAllocation optimises all the share counts together, where the greedy method takes one asset at a time. It needs a mixed-integer solver, and we use HiGHS. The table compares the cash left over and the rounding error of the two methods.

mip_slv = Solver(; name = :highs, solver = HiGHS.Optimizer,                 settings = Dict("log_to_console" => false))discrete = optimise(DiscreteAllocation(; slv = mip_slv),                    FiniteAllocationInput(; w = res.w, prices = prices, cash = cash))pretty_table(DataFrame("Method" => ["Greedy", "Discrete (MIP)"],                       "Leftover cash" => [greedy.cash, discrete.cash],                       "Rounding error" =>                           [rounding_error(greedy), rounding_error(discrete)]);             formatters = [cashfmt],             title = "Leftover cash and rounding error of the greedy and discrete allocations")
Leftover cash and rounding error of the greedy and discrete allocations
┌────────────────┬───────────────────────────────┬────────────────┐
│         Method │                 Leftover cash │ Rounding error │
│         String │                       Float64 │        Float64 │
├────────────────┼───────────────────────────────┼────────────────┤
│         Greedy │ ```math(round(v, digits = 2)) │         0.33 % │
│ Discrete (MIP) │ ```math(round(v, digits = 2)) │         0.33 % │
└────────────────┴───────────────────────────────┴────────────────┘

4. Lot sizes

Many instruments trade in lots, not single shares. GreedyAllocation(; unit = u) rounds to multiples of u shares. A larger lot gives a larger rounding error and can leave more cash unspent. The table compares single shares with lots of ten.

greedy_lots = optimise(GreedyAllocation(; unit = 10),                       FiniteAllocationInput(; w = res.w, prices = prices, cash = cash))pretty_table(DataFrame("Allocation" => ["Single shares", "Lots of 10"],                       "Leftover cash" => [greedy.cash, greedy_lots.cash],                       "Rounding error" =>                           [rounding_error(greedy), rounding_error(greedy_lots)]);             formatters = [cashfmt],             title = "Leftover cash and rounding error of the greedy allocation in single shares and in lots of 10")
Leftover cash and rounding error of the greedy allocation in single shares and in lots of 10
┌───────────────┬───────────────────────────────┬────────────────┐
│    Allocation │                 Leftover cash │ Rounding error │
│        String │                       Float64 │        Float64 │
├───────────────┼───────────────────────────────┼────────────────┤
│ Single shares │ ```math(round(v, digits = 2)) │         0.33 % │
│    Lots of 10 │ ```math(round(v, digits = 2)) │        4.067 % │
└───────────────┴───────────────────────────────┴────────────────┘

5. The budget sets the rounding error

A rounding error too small to matter on a large account is large on a small one. We allocate the same target with budgets of $100,000, $25,000 and $5,000, and the table prints the cash left over and the rounding error of each.

budgets = [100_000.0, 25_000.0, 5_000.0]budget_allocs = [optimise(GreedyAllocation(),                          FiniteAllocationInput(; w = res.w, prices = prices, cash = c))                 for c in budgets]pretty_table(DataFrame("Budget" => budgets,                       "Leftover cash" => [a.cash for a in budget_allocs],                       "Rounding error" => [rounding_error(a) for a in budget_allocs]);             formatters = [cashfmt], title = "Rounding error by budget")
                  Rounding error by budget
┌──────────┬───────────────────────────────┬────────────────┐
│   Budget │                 Leftover cash │ Rounding error │
│  Float64 │                       Float64 │        Float64 │
├──────────┼───────────────────────────────┼────────────────┤
│ 100000.0 │ ```math(round(v, digits = 2)) │         0.33 % │
│  25000.0 │ ```math(round(v, digits = 2)) │        1.744 % │
│   5000.0 │ ```math(round(v, digits = 2)) │        9.469 % │
└──────────┴───────────────────────────────┴────────────────┘

FiniteAllocationInput also takes a fees keyword, a Fees, which needs a horizon in periods too. With fees, both allocators choose share counts whose cost and fee fit in the cash. Fees and net returns covers the fees.

6. Target and allocated weights

The plot puts the target weights and the weights of the two allocations side by side.

plot_stacked_bar_composition([res, greedy, discrete], rd;                             xticks = (1:3, ["Target", "Greedy", "Discrete"]))
Example block output

This page was generated using Literate.jl.