Portfolio and asset fees
In active, and small quantity investing, fees can be a non-negligible factor that affects portfolio returns. PortfolioOptimisers.jl has the capability of including a variety of fees.
PortfolioOptimisers.FeesEstimator — Type
struct FeesEstimator{__T_tn, __T_l, __T_s, __T_fl, __T_fs, __T_lq, __T_flq, __T_dl, __T_ds, __T_dfl, __T_dfs, __T_fa, __T_kwargs} <: AbstractEstimatorNames the per-asset fee rates, for fees_constraints to align to a universe.
Every fee field accepts a dictionary, a pair, or a vector of pairs keyed by asset or group name, and the matching d* field fills every asset the keys miss. Each default fills only its own field: l draws on dl, s on ds, fl on dfl and fs on dfs, and never on a neighbour's. fees_constraints resolves the names against a UniverseSets and returns a Fees, whose fee fields are plain per-asset vectors and whose kwargs is the kwargs of this estimator.
l, s and tn are rates per period, and each of them charges on every observation of a return series. fa reaches none of the three. fl and fs charge each non-zero position one time for the whole holding period. A return series reads them as a fraction of capital, and the finite allocation reads them as a currency amount. fa names the clock they fall on, and a nothing fa charges them on the first observation. The units of the fees and returns must also be consistent.
Fields
tn: Turnover estimator or result.
l: Long proportional fees.
s: Short proportional fees.
fl: Long fixed fees.
fs: Short fixed fees.
lq: Proportional liquidation fees, charged when a position leaves the Investable Mask. The carrier lives on the complement of the mask, so its entries are the assets that left and not the assets held. A forced exit trades to zero, so the charge is the rate times the absolute previous weight, and it falls on every period besidel,sandtn.
flq: Fixed liquidation fees, charged when a position leaves the Investable Mask. The carrier lives on the complement of the mask, aslqdoes. The amount is charged once for each entry whose absolute previous weight is notisapproxto zero underkwargs, and it falls on the clockfanames besideflandfs.
dl: Default long proportional fee.
ds: Default short proportional fee.
dfl: Default long fixed fee.
dfs: Default short fixed fee.
fa: Fee amortisation algorithm, and the clock the two fixed fee terms fall on.nothingand aFirstObservationFeescharge them one time, on the first observation of a return series. AnAmortisedFeesspreads them evenly over the observation count the charging site hands in. It reaches no other term, becausel,sandtnare rates per period.
kwargs: Named tuple of keyword arguments for fee computation.
Constructors
FeesEstimator(; tn::Option{<:TnE_Tn} = nothing, l::Option{<:EstValType{<:VectorAbstractEstimatorValueAlgorithm}} = nothing, s::Option{<:EstValType{<:VectorAbstractEstimatorValueAlgorithm}} = nothing, fl::Option{<:EstValType{<:VectorAbstractEstimatorValueAlgorithm}} = nothing, fs::Option{<:EstValType{<:VectorAbstractEstimatorValueAlgorithm}} = nothing, lq::Option{<:TnE_Tn} = nothing, flq::Option{<:TnE_Tn} = nothing, dl::Option{<:Number} = nothing, ds::Option{<:Number} = nothing, dfl::Option{<:Number} = nothing, dfs::Option{<:Number} = nothing, fa::Option{<:AbstractFeeAmortisation} = nothing, kwargs::NamedTuple = (; atol = 1e-8)) -> FeesEstimatorKeywords correspond to the struct's fields.
Validation
l,s,fl,fs,dl,ds,dfl,dfsare validated withassert_nonempty_nonneg_finite_val.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
tn: Recursively updated viafactory.lq: Recursively updated viafactory.flq: Recursively updated viafactory.
View parameters
This type spans two axes, so port_opt_view is written by hand for it rather than generated from @vprop tags, and no field carries one. tn, l, s, fl and fs price the positions the portfolio holds, so they are sliced to the selected indices. lq and flq price the positions it is forced to sell, so they are sliced to the complement of those indices, which the verb derives from the width of the unreduced returns matrix it is handed:
tn: Recursively viewed at the selected indices viaport_opt_view.l: Sliced to the selected indices.s: Sliced to the selected indices.fl: Sliced to the selected indices.fs: Sliced to the selected indices.lq: Recursively viewed at the complement of the selected indices.flq: Recursively viewed at the complement of the selected indices.
Examples
julia> FeesEstimator(; tn = TurnoverEstimator(; w = [0.2, 0.3, 0.5], val = Dict("A" => 0.1)), l = Dict("A" => 0.001, "B" => 0.002), s = ["A" => 0.001, "B" => 0.002], fl = Dict("A" => 5.0), fs = ["B" => 10.0])FeesEstimator tn ┼ TurnoverEstimator │ w ┼ Vector{Float64}: [0.2, 0.3, 0.5] │ val ┼ Dict{String, Float64}: Dict("A" => 0.1) │ dval ┼ nothing │ fixed ┴ Bool: false l ┼ Dict{String, Float64}: Dict("B" => 0.002, "A" => 0.001) s ┼ Vector{Pair{String, Float64}}: ["A" => 0.001, "B" => 0.002] fl ┼ Dict{String, Float64}: Dict("A" => 5.0) fs ┼ Vector{Pair{String, Float64}}: ["B" => 10.0] lq ┼ nothing flq ┼ nothing dl ┼ nothing ds ┼ nothing dfl ┼ nothing dfs ┼ nothing fa ┼ nothing kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,)Related
PortfolioOptimisers.Fees — Type
struct Fees{__T_tn, __T_l, __T_s, __T_fl, __T_fs, __T_lq, __T_flq, __T_fa, __T_kwargs, __T_imsk} <: AbstractResultCharges a portfolio a turnover fee, a long or short proportional fee, and a long or short fixed fee.
Each of the three components keys on the sign of the position, not on the sign of the trade: an asset held long pays l and fl, an asset held short pays s and fs, whatever the direction of the rebalance that reached it. The turnover component is the one that keys on the trade, and it carries a Turnover whose val is read as a per-asset fee rate rather than as a bound.
A fixed fee is charged per position held, whatever its size. The position must be non-zero to attract one, and kwargs is forwarded to isapprox to decide how near zero counts as zero.
Fee values can be specified as scalars (applied to all assets) or as vectors of per-asset values. The portfolio fees are computed by calc_fees and asset fees by calc_asset_fees.
l, s and tn are rates per period, and each of them charges on every observation of a return series. fa reaches none of the three. fl and fs charge each non-zero position one time for the whole holding period. A return series reads them as a fraction of capital, and the finite allocation reads them as a currency amount. fa names the clock they fall on, and a nothing fa charges them on the first observation. The units of the fees and returns must also be consistent.
The axis a fee is on
A caller states every field over the full universe, the two liquidation carriers lq and flq included, because a caller cannot know which asset will delist. A door then places the fee on the axes the Investable Mask leaves through investable_fees_view: the five holding fields on the investable assets, the two carriers on the complement. The door records the mask it reduced on in imsk, so the fee it returns can say which axes it is on. A nothing imsk means the fee is a caller's statement on the full universe; a BitVector means a door has run. A caller never writes the field: an unmarked fee that meets a door with no mask has no asset that left, so the door drops its carriers rather than charging the whole book as a forced exit, and a marked fee that meets the door a second time passes through it untouched.
Mathematical definition
Portfolio fees
For non-finite optimisations, the total portfolio transaction fees are computed as:
\[\begin{align} F_{\text{t}}(\boldsymbol{w}) &\coloneqq F_{\text{Tn}} + F_{\text{p}} + F_{\text{f}} \\ F_{\text{Tn}}(\boldsymbol{w}) &= \boldsymbol{Tn} \cdot \boldsymbol{f}_{\text{Tn}}\\ F_{\text{p}}(\boldsymbol{w}) &= \left(1\left\{\boldsymbol{w} \geq 0\right\} \odot \boldsymbol{w}\right) \cdot \boldsymbol{f}_{\text{p}}^{+} - \left(1\left\{\boldsymbol{w} \lt 0\right\} \odot \boldsymbol{w}\right) \cdot \boldsymbol{f}_{\text{p}}^{-} \\ F_{\text{f}}(\boldsymbol{w}) &= 1\left\{\boldsymbol{w} \geq 0 \land \boldsymbol{w} \neq 0\right\} \cdot \boldsymbol{f}_{\text{f}}^{+} + 1\left\{\boldsymbol{w} \lt 0 \land \boldsymbol{w} \neq 0\right\} \cdot\boldsymbol{f}_{\text{f}}^{-} \end{align}\]
The finite allocation charges the same three terms on the money it actually buys. It holds the share counts and the prices, so $\boldsymbol{x} \odot \boldsymbol{p}$ is the money in each position exactly. set_allocation_fees! writes the terms in the allocation's own variables, and the charge enters the budget constraint rather than the cash. A return series reads fl and fs as a fraction of capital, and the allocation reads them as a currency amount.
Per asset fees
It is also possible to compute per-asset fees incurred using the same definitions as above, but replacing the dot products with elementwise (Hadamard) products.
\[\begin{align} \boldsymbol{F}_{\text{t}}(\boldsymbol{w}) &\coloneqq \boldsymbol{F}_{\text{Tn}} + \boldsymbol{F}_{\text{p}} + \boldsymbol{F}_{\text{f}} \\ \boldsymbol{F}_{\text{Tn}}(\boldsymbol{w}) &= \boldsymbol{Tn} \odot \boldsymbol{f}_{\text{Tn}}\\ \boldsymbol{F}_{\text{p}}(\boldsymbol{w}) &= \left(1\left\{\boldsymbol{w} \geq 0\right\} \odot \boldsymbol{w}\right) \odot \boldsymbol{f}_{\text{p}}^{+} - \left(1\left\{\boldsymbol{w} \lt 0\right\} \odot \boldsymbol{w}\right) \odot \boldsymbol{f}_{\text{p}}^{-} \\ \boldsymbol{F}_{\text{f}}(\boldsymbol{w}) &= 1\left\{\boldsymbol{w} \geq 0 \land \boldsymbol{w} \neq 0\right\} \odot \boldsymbol{f}_{\text{f}}^{+} + 1\left\{\boldsymbol{w} \lt 0 \land \boldsymbol{w} \neq 0\right\} \odot\boldsymbol{f}_{\text{f}}^{-} \end{align}\]
- $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
- $F$: Portfolio fee.
- $\boldsymbol{F}$:
N × 1per asset vector of portfolio fees. - $\boldsymbol{f}$:
N × 1per asset fee vector. If it is a scalar, it is broadcasted to all assets. - $\boldsymbol{Tn}$:
N × 1turnover vector as defined inTurnover. The benchmark weight vector is encoded in thewfield of the turnover object and the new weight vector is the portfolio weight vector. - $+,\, -$: Superscripts denote long and short fees respectively. This is because brokers sometimes charge different fees for long and short positions.
- $\text{t},\, \text{Tn},\, \text{p},\, \text{f}$: Subscripts for total, turnover, proportional, and fixed fees respectively. The turnover fee is encoded an instance of
Turnover, wherevalis the per asset fee. - $1\left\{\cdot\right\}$: Elementwise (Hadamard) indicator function returning
1when the condition is true,0otherwise. This activates long or short fees based on whether the asset weight is non-negative or otherwise. - $\boldsymbol{w} \neq 0$: Read as
!isapprox(w, 0; kwargs...), sokwargsdecides how near zero counts as zero. Only the fixed terms carry it: a proportional fee on a zero weight is zero anyway. - $\odot$: Elementwise (Hadamard) product.
The short proportional term is subtracted. $\boldsymbol{w}$ is negative wherever its indicator fires, so the minus sign is what makes the fee a positive charge.
The per asset fees sum to the portfolio fee
The two families compute one definition. calc_asset_fees splits over the assets what calc_fees contracts into a scalar, so the entries of the vector sum to the scalar. The sums differ in the order in which they add, so the identity holds to rounding and not to ==.
The JuMP model charges the same fee only when the decomposition is pinned
set_non_fixed_fees! writes the proportional terms against the model's lw and sw variables rather than against $\boldsymbol{w}$, and it writes no fixed term at all — a fixed fee needs a binary and is emitted by the MIP builder instead.
Under a PartsBoundWeights head those variables only bound the parts of $\boldsymbol{w}$, so the model's fee is an upper bound on this definition: the budget pins sum(lw) and sum(sw) whether or not a short position is held, so the model charges both sides in full.
Setting xbgt = true on the JuMPOptimiser pins the decomposition, and the model's fee then agrees with this definition. It writes binaries, so the same problem then needs a mixed-integer conic solver rather than a conic one.
A long-only model needs no pinning, because it holds no short side to bound.
Fee amortisation
l, s and tn are rates per period. Each of them charges one time per observation of a return series, and fa reaches none of them.
fl and fs are currency amounts charged one time for the whole holding period, and the fa field names the clock they fall on. Both calc_fees and calc_asset_fees return a pair, (amortised, one_time).
A nothing fa puts the two fixed charges in one_time, and charge_fees subtracts that from the first observation alone. An AmortisedFees divides them by the observation count and adds them to amortised, so every observation carries an equal share and one_time is zero.
The field carries no count. Every site that charges a fee knows the count it charges over and hands it in, so the count is never stored and never stale. The JuMP model states the same rule, through :one_time_fees and charge_one_time_fees.
The two clocks charge the same total over a horizon of T observations, which is the number calc_total_fees reports. They give a different drawdown, because one charges the whole cost on one observation and the other charges a fraction of it on each.
Fields
tn: Turnover result.
l: Long proportional fees.
s: Short proportional fees.
fl: Long fixed fees.
fs: Short fixed fees.
lq: Proportional liquidation fees, charged when a position leaves the Investable Mask. The carrier lives on the complement of the mask, so its entries are the assets that left and not the assets held. A forced exit trades to zero, so the charge is the rate times the absolute previous weight, and it falls on every period besidel,sandtn.
flq: Fixed liquidation fees, charged when a position leaves the Investable Mask. The carrier lives on the complement of the mask, aslqdoes. The amount is charged once for each entry whose absolute previous weight is notisapproxto zero underkwargs, and it falls on the clockfanames besideflandfs.
fa: Fee amortisation algorithm, and the clock the two fixed fee terms fall on.nothingand aFirstObservationFeescharge them one time, on the first observation of a return series. AnAmortisedFeesspreads them evenly over the observation count the charging site hands in. It reaches no other term, becausel,sandtnare rates per period.
kwargs: Named tuple of keyword arguments for fee computation.
imsk: The Investable Mask this fee was reduced on, ornothingwhen it is a caller's statement over the full universe. A door writes it,investable_fees_view, and a caller never does. When it is set,tn,l,s,flandfsare onfindall(imsk)andlq,flqon its complement; a door that meets the fee a second time then passes it through, where it would drop or re-slice a caller's carriers.
Constructors
Fees(; tn::Option{<:Turnover} = nothing, l::Option{<:Num_VecNum} = nothing, s::Option{<:Num_VecNum} = nothing, fl::Option{<:Num_VecNum} = nothing, fs::Option{<:Num_VecNum} = nothing, lq::Option{<:Turnover} = nothing, flq::Option{<:Turnover} = nothing, fa::Option{<:AbstractFeeAmortisation} = nothing, kwargs::NamedTuple = (; atol = 1e-8), imsk::Option{<:BitVector} = nothing) -> FeesKeywords correspond to the struct's fields.
Validation
l,s,fl,fsare validated withassert_nonempty_nonneg_finite_val.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
tn: Recursively updated viafactory.lq: Recursively updated viafactory.flq: Recursively updated viafactory.
View parameters
This type spans two axes, so port_opt_view is written by hand for it rather than generated from @vprop tags, and no field carries one. tn, l, s, fl and fs price the positions the portfolio holds, so they are sliced to the selected indices. lq and flq price the positions it is forced to sell, so they are sliced to the complement of those indices, which the verb derives from the width of the unreduced returns matrix it is handed:
tn: Recursively viewed at the selected indices viaport_opt_view.l: Sliced to the selected indices.s: Sliced to the selected indices.fl: Sliced to the selected indices.fs: Sliced to the selected indices.lq: Recursively viewed at the complement of the selected indices.flq: Recursively viewed at the complement of the selected indices.imsk: Set tonothing. The view slices; the door,investable_fees_view, is what writes the mark.
imsk prints only when it is set, so a fee a caller wrote renders as it did before the field existed; see show_fields.
Examples
julia> Fees(; tn = Turnover(; w = [0.2, 0.3, 0.5], val = [0.1, 0.0, 0.0]), l = [0.001, 0.002, 0.0], s = [0.001, 0.002, 0.0], fl = [5.0, 0.0, 0.0], fs = [0.0, 10.0, 0.0])Fees tn ┼ Turnover │ w ┼ Vector{Float64}: [0.2, 0.3, 0.5] │ val ┼ Vector{Float64}: [0.1, 0.0, 0.0] │ fixed ┴ Bool: false l ┼ Vector{Float64}: [0.001, 0.002, 0.0] s ┼ Vector{Float64}: [0.001, 0.002, 0.0] fl ┼ Vector{Float64}: [5.0, 0.0, 0.0] fs ┼ Vector{Float64}: [0.0, 10.0, 0.0] lq ┼ nothing flq ┼ nothing fa ┼ nothing kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,)Related
add_to_fees!set_non_fixed_fees!set_long_non_fixed_fees!set_short_non_fixed_fees!set_turnover_fees!FeesEstimatorOptionTurnoverNum_VecNumAbstractResultAbstractFeeAmortisationAmortisedFeescharge_feesassert_nonempty_nonneg_finite_valfees_constraintscalc_feescalc_asset_feescalc_net_returnsset_non_fixed_fees!PartsBoundWeightsJuMPOptimiserMeanRiskVariancefactoryport_opt_viewinvestable_fees_viewlift_feesshow_fields
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equation 9.14. The book states the linear variable transaction cost, which is the turnover term with equal rates on the two sides of the trade. The long/short proportional and fixed fees here key on the sign of the position rather than of the trade, and are a generalisation of it.
PortfolioOptimisers.AmortisedFees — Type
struct AmortisedFees <: AbstractFeeAmortisationSpreads the one-off terms of a fee, the two fixed charges fl and fs, evenly over a holding period.
The algorithm carries no number. Every site that charges a fee knows the observation count it charges over, and hands it in: charge_fees hands the length of the series, calc_total_fees takes the horizon as an argument, and the model hands the observation count of the fit. So the count of the holding period is never stored, and never stale.
FirstObservationFees names the other clock, which charges the two fixed terms one time, on the first observation. A nothing fa names that clock too.
The turnover charge tn is a rate per period, so it charges on every observation beside l and s. This algorithm never divides it.
Constructors
AmortisedFees() -> AmortisedFeesExamples
julia> AmortisedFees()AmortisedFees()Related
PortfolioOptimisers.FirstObservationFees — Type
struct FirstObservationFees <: AbstractFeeAmortisationCharges the one-off terms of a fee, the two fixed charges fl and fs, on the first observation of a return series.
The algorithm carries no number, and it names the clock a nothing fa names. On a Fees it is therefore a synonym for nothing, and it exists so that a caller who must state this clock has a word for it. A cross-validation scheme's fa field is that caller: nothing there means inherit the fee's own clock, so the two answers a Fees spells one way each need two words.
The turnover charge tn is a rate per period, so it charges on every observation beside l and s. This algorithm never moves it.
Constructors
FirstObservationFees() -> FirstObservationFeesExamples
julia> FirstObservationFees()FirstObservationFees()Related
PortfolioOptimisers.fees_constraints — Function
fees_constraints(fees::FeesEstimator, sets::UniverseSets; datatype::DataType = Float64,
strict::Bool = false)Resolve the name-keyed fee fields of a FeesEstimator against a universe, giving a Fees of plain per-asset vectors.
Ten fields carry the specification and each of the four proportional and fixed fields draws its gaps from its own default: l from dl, s from ds, fl from dfl and fs from dfs. A default never fills a neighbour's field. The nested tn resolves through turnover_constraints, so a FeesEstimator holding a TurnoverEstimator returns a Fees holding a Turnover. fees.fa carries no universe-keyed specification, so it reaches the result unchanged.
Algorithm
- Resolve
fees.tnagainst the universe ofsetswithturnover_constraints, giving aTurnoverwhosevalis one turnover fee rate per asset. Anothingtnstaysnothing. - Resolve
fees.lwithestimator_to_val, givingl, one long proportional rate per asset in the order of the universe. Every asset the keys miss takesfees.dl, orzero(datatype)whenfees.dlisnothing. Anothingfees.lstaysnothing. - Resolve
fees.sthe same way againstfees.ds, givings. - Resolve
fees.flthe same way againstfees.dfl, givingfl. - Resolve
fees.fsthe same way againstfees.dfs, givingfs. - Build a
Feesfrom the five resolved fields,fees.faunchanged, andfees.kwargs, which reaches the result unchanged and sets the boundary the fixed terms read.
Arguments
fees:FeesEstimatorspecifying turnover, proportional, and fixed fee values.sets:UniverseSetscontaining asset names or indices.datatype: Output data type for fee values.strict: Iftrue, enforces strict matching between assets and fee values (throws error on mismatch); iffalse, issues a warning.
Validation
- A key that names neither an asset nor a group of
setsraises anArgumentErrorwhenstrictistrue, and warns otherwise. Steps 1 to 5 each check their own field, so one bad key inlraises whatevers,flandfshold.
Returns
fe::Fees: Object containing turnover, proportional, and fixed fee values aligned withsets.
Examples
julia> sets = UniverseSets(; dict = Dict("nx" => ["A", "B", "C"]));julia> fees = FeesEstimator(; tn = TurnoverEstimator(; w = [0.2, 0.3, 0.5], val = Dict("A" => 0.1), dval = 0.0), l = Dict("A" => 0.001, "B" => 0.002), s = ["A" => 0.001, "B" => 0.002], fl = Dict("A" => 5.0), fs = ["B" => 10.0]);julia> fees_constraints(fees, sets)Fees tn ┼ Turnover │ w ┼ Vector{Float64}: [0.2, 0.3, 0.5] │ val ┼ Vector{Float64}: [0.1, 0.0, 0.0] │ fixed ┴ Bool: false l ┼ Vector{Float64}: [0.001, 0.002, 0.0] s ┼ Vector{Float64}: [0.001, 0.002, 0.0] fl ┼ Vector{Float64}: [5.0, 0.0, 0.0] fs ┼ Vector{Float64}: [0.0, 10.0, 0.0] lq ┼ nothing flq ┼ nothing fa ┼ nothing kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,)julia> fees = FeesEstimator(; tn = TurnoverEstimator(; w = [0.2, 0.3, 0.5], val = Dict("A" => 0.1), dval = 0.0, fixed = true), l = Dict("A" => 0.001, "B" => 0.002), s = ["A" => 0.001, "B" => 0.002], fl = Dict("A" => 5.0), fs = ["B" => 10.0]);julia> fees_constraints(fees, sets)Fees tn ┼ Turnover │ w ┼ Vector{Float64}: [0.2, 0.3, 0.5] │ val ┼ Vector{Float64}: [0.1, 0.0, 0.0] │ fixed ┴ Bool: true l ┼ Vector{Float64}: [0.001, 0.002, 0.0] s ┼ Vector{Float64}: [0.001, 0.002, 0.0] fl ┼ Vector{Float64}: [5.0, 0.0, 0.0] fs ┼ Vector{Float64}: [0.0, 10.0, 0.0] lq ┼ nothing flq ┼ nothing fa ┼ nothing kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,)Related
fees_constraints(fees::Option{<:Fees}, args...; kwargs...)Propagate or pass through portfolio transaction fee constraints.
fees_constraints returns the input Fees object or nothing unchanged. This method is used to propagate already constructed fee constraints or missing constraints, enabling composability and uniform interface handling in constraint generation workflows.
Algorithm
- Return
fees. AFeesalready carries one rate per asset, so no universe is resolved. The method reads none of its other arguments and none of its keywords.
Arguments
fees: An existingFeesobject ornothing.args...: Additional positional arguments (ignored).kwargs...: Additional keyword arguments (ignored).
Returns
fe::Option{<:Fees}: The input constraint object, unchanged.
Examples
julia> fees = Fees(; tn = Turnover(; w = [0.2, 0.3, 0.5], val = [0.1, 0.0, 0.0]), l = [0.001, 0.002, 0.0]);julia> fees_constraints(fees)Fees tn ┼ Turnover │ w ┼ Vector{Float64}: [0.2, 0.3, 0.5] │ val ┼ Vector{Float64}: [0.1, 0.0, 0.0] │ fixed ┴ Bool: false l ┼ Vector{Float64}: [0.001, 0.002, 0.0] s ┼ nothing fl ┼ nothing fs ┼ nothing lq ┼ nothing flq ┼ nothing fa ┼ nothing kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,)julia> fees_constraints(nothing)Related
PortfolioOptimisers.calc_fees — Function
calc_fees(w::VecNum, ::Nothing, ::Function)
calc_fees(w::VecNum, fees::Number, op::Function)
calc_fees(w::VecNum, fees::VecNum, op::Function)Compute the proportional fees for portfolio weights.
This is one term of the total fee, not the whole fee. calc_periodic_fees calls it twice, under .>= for the long side and under .< for the short side, and negates the short call. Fees states the closed form as $F_{\text{p}}$.
Algorithm
- On a
nothingfees, returnzero(eltype(w)). The method reads neitherwnorop. - Otherwise build
idx, the mask of the assets thatopselects against a zero of the promoted element type. - On a
Numberfees, scale the selected weights by the one rate, and sum them. - On a
VecNumfees, contract the selected rates with the selected weights.
Arguments
w: Portfolio weights.fees: Scalar fee value.nothing: No proportional fee, returns zero.Number: Single fee applied to all relevant assets.VecNum: Vector of fee values per asset.
op: Function to select assets,.>=for long,<for short (ignored iffeesisnothing).
Returns
val::Number: Total proportional fee.
Examples
julia> calc_fees([0.1, 0.2], 0.01, .>=)0.003Related
calc_fees(w::VecNum, ::Nothing)
calc_fees(w::VecNum, tn::Turnover)Compute the turnover fees for portfolio weights.
This is one term of the total fee, not the whole fee. Fees states the closed form as $F_{\text{Tn}}$, and reads tn.val as a per-asset fee rate rather than as a bound. The fixed flag of Turnover reaches no method here: it decides which reference weights tn.w holds, through factory, and by the time this method runs tn.w is already the vector the fee must be charged against.
Algorithm
- On a
nothingtn, returnzero(eltype(w)). The method readswonly for its element type. - Otherwise form the traded amount per asset, the absolute difference between
wand the reference weightstn.w. - On a
Numbertn.val, sum the traded amount and scale it by the one rate. - On a
VecNumtn.val, contract the rates with the traded amount.
Steps 3 and 4 are not the same expression. They agree to rounding when tn.val is a constant vector, and they differed by 3.47e-18 on w = [0.6, -0.4, 0.0, 0.25], tn.w = [0.1, 0.2, 0.3, 0.4] and a rate of 0.02.
Arguments
w: Portfolio weights.tn: Turnover structure.nothing: No turnover fee, returns zero.tn.val::Number: Single turnover fee applied to all assets.tn.val::VecNum: Vector of turnover fees per asset.
Returns
val::Number: Turnover fee.
Examples
julia> calc_fees([0.8, 0.2], Turnover(; w = [0.0, 0.0], val = 0.02))0.02Related
calc_fees(w::VecNum, T::Number, fees::Fees)Compute total fees for portfolio weights.
Sums proportional, fixed, and turnover fees for all assets. calc_asset_fees(w::VecNum, fees::Fees) splits the same total over the assets, and its sum is this number up to the order of summation.
The verb returns a pair, (amortised, one_time). l, s and tn are rates per period, so they charge on every observation and land in amortised. fl and fs are currency amounts charged one time for the whole holding period, so fees.fa decides where they land: a nothing fa puts them in one_time, and an AmortisedFees divides them by T, adds them to amortised and leaves one_time zero.
T is the observation count the calling site charges over, and the site always knows it, so no fee stores one. charge_fees hands the length of the series it lays the pair onto, and calc_total_fees contracts the pair to the cost of a whole holding period.
Algorithm
- Charge the per period terms, the call of
calc_periodic_fees. - Charge the one-off terms, the call of
calc_one_off_fees. - On a
nothingfees.fa, return the two charges unchanged. - On an
AmortisedFeesfees.fa, divide the one-off charge byT, add it to the per period charge, and return that number beside a zero of the same type.
Arguments
w: Portfolio weights.T: Observation count the fee is charged over.fees:Feesstructure.
Returns
amortised::Number: The charge every observation carries.one_time::Number: The charge the first observation carries alone.
Examples
julia> fees = Fees(; l = [0.01, 0.02], s = [0.01, 0.02], fl = [5.0, 0.0], fs = [0.0, 10.0]);julia> calc_fees([0.1, -0.2], 21, fees)(0.005, 15.0)Related
PortfolioOptimisers.calc_fixed_fees — Function
calc_fixed_fees(w::VecNum, ::Nothing, kwargs::NamedTuple, ::Function)
calc_fixed_fees(w::VecNum, fees::Number, kwargs::NamedTuple, op::Function)
calc_fixed_fees(w::VecNum, fees::VecNum, kwargs::NamedTuple, op::Function)Compute the fixed portfolio fees for assets that have been allocated.
A fixed fee is charged per position held, whatever its size. Fees states the closed form as $F_{\text{f}}$.
Algorithm
- On a
nothingfees, returnzero(eltype(w)). The method reads neitherkwargsnorop. - Otherwise build
idx1, the mask of the assets thatopselects against a zero of the promoted element type. - Build
idx2, marking the selected positions thatisapproxdoes not call zero.kwargsis forwarded toisapprox, so itsatolsets the boundary. Under the defaultatol = 1e-8a weight of1e-9attracts no fee and a weight of1e-7attracts one. - On a
Numberfees, scale the count of the positions thatidx2marks by the one rate. - On a
VecNumfees, sum the rates of the positions thatidx2marks.
Arguments
w: Portfolio weights.fees: Scalar fee value.nothing: No proportional fee, returns zero.Number: Single fee applied to all relevant assets.VecNum: Vector of fee values per asset.
kwargs: Named tuple of keyword arguments for deciding how small an asset weight has to be before being considered zero.op: Function to select assets,.>=for long,<for short (ignored iffeesisnothing).
Returns
val::Number: Total fixed fee.
Examples
julia> calc_fixed_fees([0.1, 0.2], 0.01, (; atol = 1e-6), .>=)0.02Related
PortfolioOptimisers.calc_asset_fees — Function
calc_asset_fees(w::VecNum, ::Nothing, ::Function)
calc_asset_fees(w::VecNum, fees::Number, op::Function)
calc_asset_fees(w::VecNum, fees::VecNum, op::Function)Compute the proportional per asset fees for portfolio weights.
This is one term of the total fee, not the whole fee. It is the elementwise form of calc_fees(w::VecNum, fees::Number, op::Function), and Fees states the closed form as $\boldsymbol{F}_{\text{p}}$.
Algorithm
- Allocate
fees_w, a vector of zeros one entry long per asset, in the promoted element type. An asset the mask of step 3 leaves out keeps its zero. - On a
nothingfees, returnfees_w. The method reads neitherwnoropbeyond the element type ofw. - Otherwise build
idx, the mask of the assets thatopselects against a zero of the promoted element type. - On a
Numberfees, write the selected weights, scaled by the one rate, into the selected entries offees_w. - On a
VecNumfees, write the selected weights, multiplied elementwise by the selected rates, into the selected entries offees_w.
Arguments
w: Portfolio weights.fees: Scalar fee value.nothing: No proportional fee, returns zero.Number: Single fee applied to all relevant assets.VecNum: Vector of fee values per asset.
op: Function to select assets,.>=for long,<for short (ignored iffeesisnothing).
Returns
val::VecNum: Total proportional per asset fee.
Examples
julia> calc_asset_fees([0.1, 0.2], 0.01, .>=)2-element Vector{Float64}: 0.001 0.002Related
calc_asset_fees(w::VecNum, ::Nothing)
calc_asset_fees(w::VecNum, tn::Turnover)Compute the per asset turnover fees for portfolio weights.
This is one term of the total fee, not the whole fee. It is the elementwise form of calc_fees(w::VecNum, tn::Turnover), and Fees states the closed form as $\boldsymbol{F}_{\text{Tn}}$. The fixed flag of Turnover reaches no method here, for the reason that name gives.
Algorithm
- On a
nothingtn, return a vector of zeros one entry long per asset, in the element type ofw. - Otherwise form the traded amount per asset, the absolute difference between
wand the reference weightstn.w. - On a
Numbertn.val, scale the traded amount by the one rate. - On a
VecNumtn.val, multiply the traded amount elementwise by the per-asset rates.
Arguments
w: Portfolio weights.tn: Turnover structure.nothing: No turnover fee, returns zero.tn.val::Number: Single turnover fee applied to all assets.tn.val::VecNum: Vector of turnover fees per asset.
Returns
val::VecNum: Per asset turnover fee.
Examples
julia> calc_asset_fees([0.1, 0.2], Turnover(; w = [0.0, 0.0], val = 0.01))2-element Vector{Float64}: 0.001 0.002Related
calc_asset_fees(w::VecNum, T::Number, fees::Fees)Compute total per asset fees for portfolio weights.
Sums proportional, fixed, and turnover fees for all assets. Each half sums to the matching half of the pair calc_fees(w::VecNum, T::Number, fees::Fees) returns, up to the order of summation.
The verb returns a pair, (amortised, one_time), and each half is itself a pair, one entry per axis of a reduced Fees: the charge of the assets that stayed, and the charge of the assets that left. l, s and tn are rates per period and land in amortised, as lq does on the other axis. fl and fs are currency amounts charged one time for the whole holding period, so fees.fa decides where they land, as it does for flq: a nothing fa puts them in one_time, and an AmortisedFees divides them by T, adds them to amortised and leaves one_time zero. The doctest below shows all four vectors.
T is the observation count the calling site charges over, and the site always knows it, so no fee stores one. charge_asset_fees hands the row count of the matrix it lays the pair onto, and calc_total_asset_fees contracts the pair to the cost of a whole holding period.
Algorithm
- Charge the per period terms, the call of
calc_asset_periodic_fees, which answers both axes. - Charge the one-off terms, the call of
calc_asset_one_off_fees, which answers both axes. - On a
nothingfees.fa, return the two charges unchanged. - On an
AmortisedFeesfees.fa, divide each one-off charge byT, add it to the per period charge of its own axis, and return those beside zeros of the same type. The liquidation axis adds throughadd_liquidation_terms, becauselqandflqare set independently and an unset one is an empty vector.
Arguments
w: Portfolio weights.T: Observation count the fee is charged over.fees:Feesstructure.
Returns
amortised::Tuple{<:VecNum, <:VecNum}: The charge every observation carries, per asset that stayed and per asset that left.one_time::Tuple{<:VecNum, <:VecNum}: The charge the first observation carries alone, on the same two axes. Zero under anAmortisedFees, which spread it intoamortised.
Examples
julia> fees = Fees(; l = [0.01, 0.02], s = [0.01, 0.02], fl = [5.0, 0.0], fs = [0.0, 10.0]);julia> calc_asset_fees([0.1, -0.2], 21, fees)(([0.001, 0.004], Float64[]), ([5.0, 10.0], Float64[]))Related
PortfolioOptimisers.calc_asset_fixed_fees — Function
calc_asset_fixed_fees(w::VecNum, ::Nothing, kwargs::NamedTuple, ::Function)
calc_asset_fixed_fees(w::VecNum, fees::Number, kwargs::NamedTuple, op::Function)
calc_asset_fixed_fees(w::VecNum, fees::VecNum, kwargs::NamedTuple, op::Function)Compute the per asset fixed portfolio fees for assets that have been allocated.
This is the elementwise form of calc_fixed_fees, and its entries sum to the number that name returns. Fees states the closed form as $\boldsymbol{F}_{\text{f}}$.
Algorithm
- Allocate
fees_w, a vector of zeros one entry long per asset, in the promoted element type. An asset the masks of steps 3 and 4 leave out keeps its zero. - On a
nothingfees, returnfees_w. The method reads neitherkwargsnorop. - Otherwise build
idx1, the mask of the assets thatopselects against a zero of the promoted element type. - Build
idx2, marking the selected positions thatisapproxdoes not call zero.kwargsis forwarded toisapprox, so itsatolsets the boundary. - On a
Numberfees, write the one rate, gated byidx2, into the selected entries offees_w. - On a
VecNumfees, write the selected per-asset rates, gated byidx2, into the selected entries offees_w.
Arguments
w: Portfolio weights.fees: Scalar fee value.nothing: No proportional fee, returns zero.Number: Single fee applied to all relevant assets.VecNum: Vector of fee values per asset.
kwargs: Named tuple of keyword arguments for deciding how small an asset weight has to be before being considered zero.op: Function to select assets,.>=for long,<for short (ignored iffeesisnothing).
Returns
val::VecNum: Total per asset fixed fee.
Examples
julia> calc_asset_fixed_fees([0.1, 0.2], 0.01, (; atol = 1e-6), .>=)2-element Vector{Float64}: 0.01 0.01Related
PortfolioOptimisers.calc_total_fees — Function
calc_total_fees(w::VecNum, T::Number, fees::Option{<:Fees})Charge the whole cost of holding a portfolio for T periods.
calc_fees answers one observation of a return series. This verb answers the whole holding period, so it charges the per period terms T times and the one-off terms one time. It needs the rates, the fixed amounts and the horizon, and nothing else. fees.fa reaches no term here, because that field names where a one-off cost lands on a return series, and this verb reports no series. The finite allocation reads this verb to take the fees out of the cash before it allocates.
Algorithm
- Charge
Ttimes the per period terms, the call ofcalc_periodic_fees. - Charge the one-off terms one time, the call of
calc_one_off_fees. - Return the sum of the two terms.
Arguments
w: Portfolio weights.T: Horizon, in periods.fees:Feesstructure, ornothing.
Returns
val::Number: The whole cost of the holding period.
Examples
julia> fees = Fees(; l = 0.01, fl = 5.0, tn = Turnover(; w = [0.0, 0.0], val = 0.002));julia> calc_total_fees([0.5, 0.5], 252, fees)13.024000000000001Related
PortfolioOptimisers.calc_total_asset_fees — Function
calc_total_asset_fees(w::VecNum, T::Number, fees::Option{<:Fees})Split over the assets the whole cost of holding a portfolio for T periods.
The per asset twin of calc_total_fees. Its entries sum to that number, up to the order of summation.
Algorithm
- Charge
Ttimes the per period terms, the call ofcalc_asset_periodic_fees. Both halves of its pair are scaled, because both are rates. - Charge the one-off terms one time, the call of
calc_asset_one_off_fees. Neither half is scaled, because both are currency amounts. - Return the pair, each axis summed with its own half: the investable total, and the liquidation total through
add_liquidation_terms, which answers the axis whose two terms are set independently.
Arguments
w: Portfolio weights.T: Horizon, in periods.fees:Feesstructure, ornothing.
Returns
The verb returns a pair, one entry per axis of a reduced Fees.
investable::VecNum: The whole cost of the holding period for each asset that stayed, froml,s,tn,flandfs, which were sliced to the Investable Mask.liquidation::VecNum: The whole cost of the forced exit of each asset that left, fromlqandflq, which were sliced to the mask's complement. Empty when neither carrier is set.
Examples
julia> fees = Fees(; l = 0.01, fl = 5.0, tn = Turnover(; w = [0.0, 0.0], val = 0.002));julia> calc_total_asset_fees([0.5, 0.5], 252, fees)([6.5120000000000005, 6.5120000000000005], Float64[])Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(fees::Fees, i, X::MatNum, args...)
port_opt_view(fees::FeesEstimator, i, X::MatNum, args...)
port_opt_view(fees::FeesE_Fees, i, args...)Sub-select a fee to the assets an optimisation keeps, on both of its axes.
A Fees spans two axes once an optimisation has reduced to its Investable Mask. tn, l, s, fl and fs price the positions the portfolio holds, so they live on the investable axis and are sliced at i. lq and flq price the positions it is forced to sell, so they live on the complement of that axis and are sliced at the assets i leaves out.
That is why neither carrier is tagged @vprop and why this verb is written by hand rather than generated: the generic machinery threads one index through every tagged field, and here the two groups need different ones. The complement is derived and never stored, from i and the width of X. X is the unreduced returns matrix, which is what investable_reduction and investable_view already pass, so size(X, 2) is the full universe. On a resolved Fees the split itself is two_axis_fees_view, which takes that width as a number so the Investable Mask door can reach it without a matrix; the FeesEstimator method takes the same split in place.
The three-argument method serves a caller that hands no matrix. It cannot derive a complement, so it slices the five per-asset fields and passes the two carriers through untouched, which is correct because they already sit on their own axis.
Neither method writes imsk: both return an unmarked fee. The view is a slice, and a cluster of a nested optimiser takes it as the Investable Mask door does, so a mark written here would make an inner fit read a cluster's complement as the assets that left. The door, investable_fees_view, is the one verb that marks a fee, and it does so after this view returns.
Algorithm
- On a
Fees, handiandsize(X, 2)totwo_axis_fees_view, which owns the split. - On a
FeesEstimator, buildj, the complement ofiin1:size(X, 2), and take the same split in place:tnatithroughport_opt_view, whose@vproptags takewand a vectorvaland leave a scalar or dictionaryvalalone;l,s,flandfsatithroughnothing_scalar_array_view; andlqandflqatj, or set tonothingon an emptyj, because no asset left and there is nothing to liquidate. - Rebuild through the keyword constructor, carrying the defaults,
faandkwargsunchanged.
Arguments
fees: The fee to reduce.i: Indices of the assets the optimisation keeps.X: The unreduced returns matrix, read for its width alone.
Returns
fees::typeof(fees): The fee on the two reduced axes, unmarked.
Related
PortfolioOptimisers.needs_previous_weights — Method
needs_previous_weights(fe::FeesE_Fees) -> BoolCheck if a fee constraint or estimator requires previous portfolio weights by calling needs_previous_weights on fe.tn.
Only the turnover term reads a previous weight vector. The proportional and fixed terms key on the sign of the position that w already carries, so a Fees whose tn is nothing needs none.
Algorithm
- Read
fe.tnand forward it toneeds_previous_weights, which answers!tn.fixedon a turnover object andfalseonnothing.
Arguments
fe: Fee constraint or estimator.
Returns
Bool:trueif previous weights are needed,falseotherwise.
Related
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).