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)).
GreedyAllocationrounds 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.DiscreteAllocationsolves 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.
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.6272. 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"]))This page was generated using Literate.jl.