The source files can be found in examples/.
Profile: desk, monthly
The second profile rebalances the book of a professional desk once a month. Every limit that binds in the retail profile loosens here. A monthly decision can pay for a long computation, and a month of return covers more trading cost than a day of it does. What this desk has instead is a house view, and the time to look at the whole risk-return trade-off before it picks a book.
Three of the limits in the strategy decision framework change for this desk.
- A monthly rebalance leaves time for a slower computation, so we trace the whole efficient frontier with fifteen solves and then take its tangency point.
- The desk has a view, so we state it as a constraint on the mean and fit an
EntropyPoolingPriorrather than take the sample mean as given. - A monthly decision can wait for a mixed-integer solve, so we buy whole shares with
DiscreteAllocation.
Reach for this profile when you trade rarely and can spend the time on the model instead. Put that time into the prior and the frontier, choose a point on it, and allocate with a mixed-integer solve. Turnover and fee control matter less when you trade once a month.
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;1. Data and the house view
The desk expects healthcare to outperform energy. We state that as a view on the mean, and EntropyPoolingPrior turns it into a new probability for each historical scenario. A scenario that supports the view gets more probability, and the rest get less. Entropy Pooling covers the method that finds those probabilities.
X = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)prices = vec(values(X)[end, :])sets = UniverseSets(; dict = Dict("nx" => rd.nx, "energy" => ["CVX", "XOM", "RRC"], "healthcare" => ["JNJ", "LLY", "MRK", "PFE", "UNH"]))view_prior = EntropyPoolingPrior(; sets = sets, mu_views = LinearConstraintEstimator(; val = ["healthcare >= energy"]))pr = prior(view_prior, rd)slv = Solver(; name = :clarabel, solver = Clarabel.Optimizer, settings = Dict("verbose" => false), check_sol = (; allow_local = true, allow_almost = true))rf = 4.2 / 100 / 2520.00016666666666666672. The efficient frontier
We solve for minimum risk fifteen times, once at each of fifteen lower bounds on the return. The fifteen books trace the efficient frontier of the prior that carries the view. The plot shows what each step up in return costs in risk.
frontier = optimise(MeanRisk(; obj = MinimumRisk(), opt = JuMPOptimiser(; pe = pr, slv = slv, ret = ArithmeticReturn(; settings = JuMPReturnsSettings(; lb = Frontier(; N = 15))))))plot_efficient_frontier(frontier.w, pr; rt = frontier.ret)3. Choosing the book
The frontier shows the trade-off, and the desk still has to pick one point on it. We solve once more with MaximumRatio, which maximises the ratio of return above the risk-free rate to risk. That book is the tangency point of the frontier.
desk = optimise(MeanRisk(; obj = MaximumRatio(; rf = rf), opt = JuMPOptimiser(; pe = pr, slv = slv)))pretty_table(DataFrame("Asset" => rd.nx, "Tangency weight" => desk.w); formatters = [resfmt], title = "Desk monthly tangency weights on the prior with the view")Desk monthly tangency weights on the prior with the view
┌────────┬─────────────────┐
│ Asset │ Tangency weight │
│ String │ Float64 │
├────────┼─────────────────┤
│ AAPL │ 0.0 % │
│ AMD │ 0.0 % │
│ BAC │ 0.0 % │
│ BBY │ 0.0 % │
│ CVX │ 0.0 % │
│ GE │ 0.0 % │
│ HD │ 0.0 % │
│ JNJ │ 0.0 % │
│ JPM │ 0.0 % │
│ KO │ 0.0 % │
│ LLY │ 0.002 % │
│ MRK │ 76.988 % │
│ MSFT │ 0.0 % │
│ PEP │ 0.0 % │
│ PFE │ 0.0 % │
│ PG │ 0.0 % │
│ RRC │ 0.0 % │
│ UNH │ 0.0 % │
│ WMT │ 0.0 % │
│ XOM │ 23.008 % │
└────────┴─────────────────┘4. Exact finite allocation
The desk has $500,000 to invest, so rounding to whole shares moves each weight by very little. DiscreteAllocation solves a mixed-integer problem with HiGHS. It minimises the money by which each position misses its target, plus the cash left over. The table title gives the amount invested and the cash left.
mip_slv = Solver(; name = :highs, solver = HiGHS.Optimizer, settings = Dict("log_to_console" => false))alloc = optimise(DiscreteAllocation(; slv = mip_slv), FiniteAllocationInput(; w = desk.w, prices = prices, cash = 500_000.0))invested = sum(alloc.shares .* prices)pretty_table(DataFrame("Asset" => rd.nx, "Target" => desk.w, "Shares" => round.(Int, alloc.shares), "Realised" => alloc.w); formatters = [resfmt], title = "\$500,000 to invest, \```math(round(Int, invested)) invested, \```(round(alloc.cash, digits = 2)) left in cash")$500,000 to invest, ```math(round(Int, invested)) invested, ```(round(alloc.cash, digits = 2)) left in cash
┌────────┬──────────┬────────┬──────────┐
│ 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 │ 0.0 % │ 0 │ 0.0 % │
│ GE │ 0.0 % │ 0 │ 0.0 % │
│ HD │ 0.0 % │ 0 │ 0.0 % │
│ JNJ │ 0.0 % │ 0 │ 0.0 % │
│ JPM │ 0.0 % │ 0 │ 0.0 % │
│ KO │ 0.0 % │ 1 │ 0.013 % │
│ LLY │ 0.002 % │ 0 │ 0.0 % │
│ MRK │ 76.988 % │ 3512 │ 76.972 % │
│ MSFT │ 0.0 % │ 0 │ 0.0 % │
│ PEP │ 0.0 % │ 0 │ 0.0 % │
│ PFE │ 0.0 % │ 0 │ 0.0 % │
│ PG │ 0.0 % │ 0 │ 0.0 % │
│ RRC │ 0.0 % │ 1 │ 0.005 % │
│ UNH │ 0.0 % │ 0 │ 0.0 % │
│ WMT │ 0.0 % │ 0 │ 0.0 % │
│ XOM │ 23.008 % │ 1079 │ 23.011 % │
└────────┴──────────┴────────┴──────────┘5. The book
plot_stacked_bar_composition([desk], rd; xticks = (1:1, ["Desk monthly"]))This page was generated using Literate.jl.