Skip to content

Optimiser

Constrained portfolio optimisation: constraints, the solver, the efficient frontier and OptimisationResult. See Optimiser.

optimise

Portfolio optimisation.

Constraint classes describing what a portfolio is allowed to be, and a solver that finds the closest feasible portfolio to a target: the tracking problem an index business meets first.

Solving needs scipy, which ships in the optimise extra:

pip install "py-beacon-kit[optimise]"

Importing this package does not need scipy. Constraints, configs and results are descriptions, and describing a problem is core work: Backtest.run takes an :class:OptimisationConfig, and the catalogue serialises constraint configurations, neither of which should cost an optional package. scipy is required at solve time, where a missing install raises MissingDependencyError naming the extra.

OptimisationConfig dataclass

OptimisationConfig(
    objective: str = MIN_TRACKING_ERROR,
    constraints: Sequence[Constraint] = (),
    risk_model: RiskModel | None = None,
)

What an optimised run is asked to do.

Attributes:

Name Type Description
objective str

What to minimise. Only "min_tracking_error" exists today, and the field names it anyway so a stored config says what it meant when other objectives arrive.

constraints Sequence[Constraint]

What the answer must satisfy, as :class:~beacon.optimise.constraints.Constraint instances. Empty means the solver's own default of full investment alone.

risk_model RiskModel | None

Reserved: carried but unused. The slot exists so covariance-aware optimised runs can arrive without changing this shape. Nothing reads it yet and passing one changes no result, though an optimised index carrying one cannot be cached.

Cardinality

Cardinality(maximum: int)

Bases: Constraint

A limit on how many names may be held.

Unlike every other constraint here this one is not convex: it counts non-zero positions, and no continuous solver can express that. It is honoured by a two-stage heuristic (see :func:beacon.optimise.solver.minimise_tracking_error): solve, keep the largest positions, then solve again with the rest pinned at zero. The answer satisfies the limit but is not proven optimal, and the exact problem is a mixed-integer program that would need a different solver entirely.

Attributes:

Name Type Description
maximum

The largest number of names that may carry weight.

solver_conditions

solver_conditions(assets: Sequence[str]) -> list[Condition]

None: counting holdings is a step function.

Its gradient is zero everywhere it is defined, so handing it to SLSQP would say that dropping a name costs nothing and changes nothing. The limit is enforced by restricting the problem instead.

conditions

conditions(assets: Sequence[str]) -> list[Condition]

The count, as an inequality that is only ever checked.

Condition dataclass

Condition(
    label: str,
    kind: str,
    evaluate: Callable[[Vector], float],
    gradient: Callable[[Vector], Vector] | None = None,
)

One scalar condition on a weight vector.

Attributes:

Name Type Description
label str

Human-readable description, carried through to the binding report so a client can show which rule bit.

kind str

EQUALITY or INEQUALITY.

evaluate Callable[[Vector], float]

The condition function. Zero when an equality holds, non-negative when an inequality holds.

gradient Callable[[Vector], Vector] | None

Derivative with respect to the weights, when one can be written down. Supplying it keeps the solver off finite differences, which on a problem this small is most of the accuracy.

Constraint

Bases: ABC

Something an optimal weight vector must satisfy.

Subclasses state their rules as conditions, and everything else (solving, binding detection, verification) is derived from those.

Attributes:

Name Type Description
UNIT str

What this constraint's slack is measured in. :data:FRACTION for everything expressed as a share of something (weights, turnover, an expected return), which is every constraint here but one, so it is the default a new subclass inherits. Override it when the quantity is not a fraction; :class:Cardinality counts names.

conditions abstractmethod

conditions(assets: Sequence[str]) -> list[Condition]

The conditions this constraint imposes over assets, in order.

Parameters:

Name Type Description Default
assets Sequence[str]

The universe, fixing the meaning of each weight position.

required

Returns:

Name Type Description
list list[Condition]

Conditions in scipy's convention.

solver_conditions

solver_conditions(assets: Sequence[str]) -> list[Condition]

The subset of the conditions that should go to the solver.

Everything by default. A constraint overrides this to nothing when it reaches the solver some other way (as a box, or by restricting the problem) while still reporting its conditions for verification.

bounds

bounds(
    assets: Sequence[str],
) -> list[tuple[float, float]] | None

Per-asset box limits, when this constraint is one.

A box handed to the solver as a bound is enforced at every iterate, whereas the same box as an inequality is only approached from outside; for position limits that difference is worth the special case.

Returns:

Type Description
list[tuple[float, float]] | None

list or None: One (low, high) per asset, or None if this

list[tuple[float, float]] | None

constraint is not a box.

report

report(
    weights: Vector, assets: Sequence[str]
) -> list[Slack]

How much room this constraint has left at weights.

Parameters:

Name Type Description Default
weights Vector

A candidate solution, aligned to assets.

required
assets Sequence[str]

The universe.

required

Returns:

Name Type Description
list list[Slack]

One Slack per condition, each stamped with this constraint's

list[Slack]

attr:UNIT.

validate

validate(assets: Sequence[str]) -> None

Raise if this constraint cannot be applied to assets at all.

Separate from feasibility: this catches a constraint that is malformed or refers to names that are not there, which is a caller mistake rather than an over-tight problem. It does nothing by default, and a subclass overrides it only when it has something to check.

ExpectedReturnTarget

ExpectedReturnTarget(
    expected_returns: dict[str, float], target: float
)

Bases: Constraint

The portfolio must be expected to return exactly this much.

The constraint that traces out a frontier: fix the return, minimise the variance, repeat. It is an equality rather than a floor because a frontier point is a specific point, and a floor would let every solve collapse onto the minimum-variance portfolio whenever that portfolio happened to clear the bar.

Attributes:

Name Type Description
expected_returns

Expected return per asset, in the same units and over the same horizon as the risk model's covariance. Annualised, if the risk model is.

target

The portfolio return being asked for.

validate

validate(assets: Sequence[str]) -> None

Every asset must have an expected return.

Treating an absent one as zero would quietly bias the whole frontier towards holding it, since a zero-return asset looks like the safest way to satisfy a low return target.

FullInvestment

FullInvestment(target: float = 1.0)

Bases: Constraint

The weights must sum to a fixed total, normally one.

Attributes:

Name Type Description
target

The total. One means fully invested with no leverage and no cash; below one leaves cash, above one is levered.

GroupBounds

GroupBounds(
    name: str,
    members: Sequence[str],
    minimum: float = 0.0,
    maximum: float = 1.0,
)

Bases: Constraint

Limits on the combined weight of a set of names.

A sector, a country, a liquidity bucket: anything the client groups by.

Attributes:

Name Type Description
name

What the group is, used in the binding report.

members

The names in it.

minimum

Smallest combined weight the group may take.

maximum

Largest combined weight the group may take.

validate

validate(assets: Sequence[str]) -> None

Reject a group with no members in the universe.

Members outside the universe are dropped rather than rejected: a sector map is defined over a whole market and is meant to be reused across indices, so naming companies this index does not hold is normal. A group that matches nothing, though, is a mistake: it would silently constrain an empty sum, which is always satisfied.

PositionBounds

PositionBounds(
    minimum: float = 0.0,
    maximum: float = 1.0,
    assets: Sequence[str] | None = None,
)

Bases: Constraint

Lower and upper limits on individual weights.

Attributes:

Name Type Description
minimum

Smallest weight any covered asset may take. Zero forbids short positions, which is the usual index-tracking case.

maximum

Largest weight any covered asset may take.

assets

Names this applies to, or None for every name. Several of these compose (a blanket rule plus a tighter one on a few names), and the tightest limit on each name wins.

validate

validate(assets: Sequence[str]) -> None

Reject a bound on a name that is not in the universe.

Unlike a group, a position bound names one asset explicitly, so a name that is not there is a typo rather than a broader definition being reused, and silently dropping it would return an answer that ignored a limit the caller asked for.

bounds

bounds(assets: Sequence[str]) -> list[tuple[float, float]]

The box, unlimited on any asset this constraint does not cover.

solver_conditions

solver_conditions(assets: Sequence[str]) -> list[Condition]

None: the solver receives this constraint as a box instead.

conditions

conditions(assets: Sequence[str]) -> list[Condition]

The same box, written as inequalities.

Not given to the solver (:meth:bounds covers that), but used to report which positions came out at a limit.

Slack dataclass

Slack(
    label: str,
    kind: str,
    slack: float,
    unit: str = FRACTION,
)

How much room a condition has left at a given solution.

Attributes:

Name Type Description
label str

The condition's description.

kind str

EQUALITY or INEQUALITY.

slack float

Signed room. For an inequality this is the condition function itself, so zero means the solution sits exactly on the boundary and negative means it has crossed. For an equality it is the negated absolute residual, which makes an exactly-satisfied equality read as zero slack, correctly, since an equality is always binding.

unit str

What slack is measured in, :data:FRACTION or :data:COUNT, copied off the constraint that produced it. Carried because a number whose unit is only knowable from the constraint's class name is a number no consumer can format.

is_binding property

is_binding: bool

Whether the solution sits on this condition's boundary.

is_violated property

is_violated: bool

Whether the solution breaks this condition.

TurnoverBudget

TurnoverBudget(
    maximum: float, current_weights: dict[str, float]
)

Bases: Constraint

A limit on how far the solution may move from the current holdings.

Turnover here is one-way: half the sum of absolute weight changes, which under full investment is the amount bought, and equally the amount sold. A budget of 5% therefore means what an index methodology means by "5% turnover", not a 2.5% round trip.

Attributes:

Name Type Description
maximum

The one-way budget, as a fraction of the portfolio.

current_weights

Where the portfolio is now. Names absent from it are treated as currently unheld.

turnover

turnover(weights: Vector, assets: Sequence[str]) -> float

One-way turnover of weights against the current holdings.

conditions

conditions(assets: Sequence[str]) -> list[Condition]

The budget as one inequality.

The absolute value has a kink wherever an asset's weight equals its current weight, so this function is not differentiable everywhere and the gradient used is a subgradient: the sign vector, which is the true derivative away from those kinks and an arbitrary choice of one at them. SLSQP assumes smoothness and can in principle stall on a solution that sits exactly on many kinks at once. In practice it converges, and the verification pass refuses the answer if it does not: an exact formulation needs auxiliary variables for the positive and negative parts, which doubles the problem and is not worth it until a real case demands it.

EfficientFrontier dataclass

EfficientFrontier(
    points: list[FrontierPoint],
    minimum_variance: FrontierPoint,
    tangency: FrontierPoint,
    risk_free_rate: float = 0.0,
)

A traced frontier and the points on it worth naming.

Attributes:

Name Type Description
points list[FrontierPoint]

The grid, in increasing order of expected return. The first is the minimum-variance portfolio and the last is the highest return the constraints allow.

minimum_variance FrontierPoint

The least risky feasible portfolio.

tangency FrontierPoint

The highest Sharpe ratio available.

risk_free_rate float

The rate the Sharpe ratios were computed against.

volatilities property

volatilities: list[float]

Each point's volatility, in grid order.

expected_returns property

expected_returns: list[float | None]

Each point's expected return, in grid order.

is_monotonic

is_monotonic(tolerance: float = 1e-07) -> bool

Whether risk rises with return across the grid.

The defining property of a frontier. It should always hold (insisting on more return can only cost risk), so a False here means a point failed to solve to optimality rather than that the curve is unusual.

to_frame

to_frame() -> pd.DataFrame

The frontier as a table, one row per point.

weights_frame

weights_frame() -> pd.DataFrame

Every point's weights, points on the index and assets on the columns.

FrontierPoint dataclass

FrontierPoint(
    weights: Series,
    volatility: float,
    expected_return: float | None = None,
    sharpe_ratio: float | None = None,
    binding: list[str] = list(),
    heuristic: bool = False,
)

One portfolio on the frontier.

Attributes:

Name Type Description
weights Series

The portfolio, indexed by asset id.

volatility float

Annualised standard deviation, in the risk model's units.

expected_return float | None

Portfolio expected return, or None when no expected returns were supplied, since a return cannot be reported if it was never given.

sharpe_ratio float | None

Excess return over volatility, or None when the expected return is unknown or the volatility is negligible.

binding list[str]

Labels of the constraints this point sits on. The interesting part of a frontier point: it says which rule is what stops the portfolio from doing better.

heuristic bool

Whether a non-convex constraint forced a restricted re-solve, so this point is feasible but not proven optimal.

BindingConstraint dataclass

BindingConstraint(
    label: str,
    kind: str,
    slack: float,
    unit: str = FRACTION,
)

A constraint the solution sits exactly on.

Binding constraints are the interesting part of an answer: they are the rules that actually cost something, and relaxing one of them is the only way to improve the objective.

Attributes:

Name Type Description
label str

What the constraint was.

kind str

EQUALITY or INEQUALITY.

slack float

Room left, at or near zero by definition of binding. Carried so a caller can see how tight "tight" was.

unit str

What slack is measured in, off the constraint that produced it. Near-meaningless here, where the number is zero by construction; it matters on the non-binding slacks in :attr:OptimisationResult .slacks, and the two carry the same field so a client formats either the same way.

OptimisationResult dataclass

OptimisationResult(
    weights: Series,
    target_weights: Series,
    binding: list[BindingConstraint],
    diagnostics: SolverDiagnostics,
    slacks: list[Slack] = list(),
    heuristic: bool = False,
    _risk_model: RiskModel | None = None,
)

Optimal weights, what they cost, and which rules bound.

Attributes:

Name Type Description
weights Series

The solution, indexed by asset id.

target_weights Series

What the solve was tracking, on the same index.

binding list[BindingConstraint]

Constraints the solution sits on, tightest first.

diagnostics SolverDiagnostics

How the solve went.

slacks list[Slack]

Every constraint's room at the solution, binding or not. Kept so a caller can see what nearly bound as well as what did.

heuristic bool

Whether a non-convex constraint forced a heuristic stage, in which case the answer satisfies every constraint but is not proven optimal.

asset_ids property

asset_ids: list[str]

Assets in the solution, in weight-vector order.

active_weights property

active_weights: Series

Solution minus target: the active position.

Sums to zero whenever both sides are fully invested to the same total, which is the usual case and worth checking: a non-zero sum means the solve was allowed to change how much is invested, not just where.

holdings property

holdings: int

How many names carry meaningful weight.

with_risk_model

with_risk_model(
    risk_model: RiskModel,
) -> OptimisationResult

Bind a risk model for risk-based accessors. Returns self.

tracking_error

tracking_error() -> float

Distance from the target, in the metric the solve minimised.

With a risk model this is annualised tracking error, the quantity a tracking mandate is measured on. Without one the objective's identity covariance makes it the Euclidean distance between the two weight vectors: a sensible thing to minimise, but not a volatility, and not comparable to a number produced with a risk model.

turnover

turnover(
    current_weights: dict[str, float] | None = None,
) -> float

One-way turnover from current_weights to the solution.

Half the summed absolute weight change, matching :class:~beacon.optimise.constraints.TurnoverBudget. Defaults to measuring against the target, which answers "how far did the optimiser move me off the index".

Parameters:

Name Type Description Default
current_weights dict[str, float] | None

Where the portfolio is now. None measures against the target weights.

None

Returns:

Name Type Description
float float

One-way turnover as a fraction of the portfolio.

to_frame

to_frame() -> pd.DataFrame

Target, optimal and active weights side by side, largest active first.

binding_labels

binding_labels() -> list[str]

Just the descriptions of the binding constraints.

SolverDiagnostics dataclass

SolverDiagnostics(
    converged: bool,
    iterations: int,
    evaluations: int,
    objective: float,
    status: int,
    message: str,
)

What the solver did.

Attributes:

Name Type Description
converged bool

Whether the solver reported success. An answer is only returned when this is True and the weights pass verification, so a caller seeing this is seeing a solve that worked.

iterations int

Major iterations taken.

evaluations int

Objective evaluations.

objective float

Final objective value. It is a variance, so the tracking error is its square root.

status int

The solver's numeric exit code.

message str

The solver's own description of how it exited.

Solution dataclass

Solution(
    weights: Vector,
    outcome: Any,
    slacks: list[Slack],
    heuristic: bool,
)

A verified answer to a constrained problem.

Attributes:

Name Type Description
weights Vector

The solution vector, aligned to the universe.

outcome Any

The solver's own result object.

slacks list[Slack]

Every constraint's room at the solution.

heuristic bool

Whether a non-convex constraint forced a restricted re-solve, in which case the answer is feasible but not proven optimal.

constraint_from_payload

constraint_from_payload(
    payload: Mapping[str, Any],
) -> Constraint

Rebuild a constraint from a {type, params} payload.

The inverse of :func:constraint_payload, and the builder the server's constraint rows go through: one code path from a stored document to the object the solver receives.

Parameters:

Name Type Description Default
payload Mapping[str, Any]

A mapping carrying type and, optionally, params.

required

Returns:

Name Type Description
Constraint Constraint

A fresh instance that solves identically to the one the

Constraint

payload was taken from.

Raises:

Type Description
CalculationError

If the type names nothing registered. A constructor rejecting the params (a missing argument, a minimum above a maximum) propagates untouched, so the class's own message reaches the caller.

constraint_payload

constraint_payload(
    constraint: Constraint,
) -> dict[str, Any]

One constraint as its registered name plus parameter values.

Parameter names come from the catalogue's constructor introspection, each value read off the instance attribute of the same name. Every registered type follows that convention, and the cache fingerprint keys by it.

Parameters:

Name Type Description Default
constraint Constraint

The instance to describe.

required

Returns:

Name Type Description
dict dict[str, Any]

{"type": class name, "params": {name: value}}, with every

dict[str, Any]

value JSON-serialisable.

Raises:

Type Description
CalculationError

If the class is not registered under the CONSTRAINT kind, keeps no attribute for one of its constructor parameters, or holds a value JSON cannot carry. Refused rather than guessed at: a payload nothing can rebuild is worse than no payload.

count_holdings

count_holdings(weights: Vector) -> int

How many positions carry meaningful weight.

one_way_turnover

one_way_turnover(weights: Vector, current: Vector) -> float

Half the summed absolute weight change between two portfolios.

The halving is what makes this one-way: under full investment every unit bought is a unit sold, so the undivided sum counts each trade twice.

default_constraints

default_constraints() -> list[Constraint]

Long-only and fully invested.

The frontier's default rather than full investment alone, because with shorting unbounded the maximum-return portfolio does not exist: short the worst asset without limit to fund the best, and the return grows forever. A frontier needs a right-hand end.

efficient_frontier

efficient_frontier(
    risk_model: RiskModel,
    expected_returns: dict[str, float],
    points: int = DEFAULT_POINTS,
    constraints: Sequence[Constraint] | None = None,
    risk_free_rate: float = 0.0,
) -> EfficientFrontier

Trace the frontier, and locate its minimum-variance and tangency points.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance. Its asset order defines the universe.

required
expected_returns dict[str, float]

Expected return per asset, in the same units and over the same horizon as the covariance: annualised, if it is.

required
points int

How many portfolios to solve for, minimum 2.

DEFAULT_POINTS
constraints Sequence[Constraint] | None

What every point must satisfy. None means long-only and fully invested.

None
risk_free_rate float

The rate Sharpe ratios are measured against.

0.0

Returns:

Name Type Description
EfficientFrontier EfficientFrontier

The grid and the two named points.

Raises:

Type Description
CalculationError

If points is below 2, if the constraints cannot be satisfied, or if the problem is unbounded above.

maximum_return_portfolio

maximum_return_portfolio(
    risk_model: RiskModel,
    expected_returns: dict[str, float],
    constraints: Sequence[Constraint] | None = None,
    risk_free_rate: float = 0.0,
) -> FrontierPoint

The highest-returning feasible portfolio.

The frontier's right-hand end. Maximising a linear objective often has many optimal solutions (any mix of the top assets, if they tie), so the return is found first and the variance minimised subject to achieving it, which picks the sensible one out of the tie.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance, used to break ties and report risk.

required
expected_returns dict[str, float]

Expected return per asset.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means long-only and fully invested.

None
risk_free_rate float

For the reported Sharpe ratio.

0.0

Returns:

Name Type Description
FrontierPoint FrontierPoint

The portfolio.

minimum_variance_portfolio

minimum_variance_portfolio(
    risk_model: RiskModel,
    constraints: Sequence[Constraint] | None = None,
    expected_returns: dict[str, float] | None = None,
    risk_free_rate: float = 0.0,
) -> FrontierPoint

The least risky feasible portfolio.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance to minimise against.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means long-only and fully invested.

None
expected_returns dict[str, float] | None

Used only to report the point's return and Sharpe ratio; it has no effect on the weights, since minimum variance ignores return by definition.

None
risk_free_rate float

For the reported Sharpe ratio.

0.0

Returns:

Name Type Description
FrontierPoint FrontierPoint

The portfolio, its risk, and which constraints bound.

Raises:

Type Description
CalculationError

If the constraints cannot be satisfied.

minimise_tracking_error

minimise_tracking_error(
    target_weights: Series | dict[str, float],
    constraints: Sequence[Constraint] | None = None,
    risk_model: RiskModel | None = None,
) -> OptimisationResult

Find the closest feasible portfolio to a target.

Parameters:

Name Type Description Default
target_weights Series | dict[str, float]

What to track, by asset id. Defines the universe: the optimiser allocates over exactly these names, in this order.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means full investment alone, which is the smallest problem that has a unique answer.

None
risk_model RiskModel | None

Covariance to measure distance with. None treats every asset as equally risky and uncorrelated, which minimises plain squared weight distance.

None

Returns:

Name Type Description
OptimisationResult OptimisationResult

Optimal weights, the active position, which

OptimisationResult

constraints bound, and how the solve went.

Raises:

Type Description
CalculationError

If the constraints cannot all be satisfied, if the solver fails to converge, or if the returned weights violate a constraint. Also if a constraint or the risk model refers to assets outside the universe.

solve_constrained

solve_constrained(
    objective: Callable[[Vector], float],
    gradient: Callable[[Vector], Vector],
    rules: Sequence[Constraint],
    assets: Sequence[str],
    hint: Vector | None = None,
) -> Solution

Minimise objective over the weights, subject to rules.

The objective-agnostic core. Tracking error is one objective; portfolio variance, expected return and the Sharpe ratio are others, and all of them want the same constraint handling, the same infeasibility messages and the same refusal to return a violating answer.

Parameters:

Name Type Description Default
objective Callable[[Vector], float]

What to minimise, as a function of the weight vector.

required
gradient Callable[[Vector], Vector]

Its derivative. Required rather than optional: finite differences on a problem this small cost more accuracy than they save effort.

required
rules Sequence[Constraint]

The constraints.

required
assets Sequence[str]

The universe, fixing the meaning of each weight position.

required
hint Vector | None

Where to start the search. None starts from equal weights.

None

Returns:

Name Type Description
Solution Solution

The verified answer.

Raises:

Type Description
CalculationError

If the constraints cannot all be satisfied, if the solver fails to converge, or if the weights violate a constraint.

config

The serialisable description of an optimisation.

A constraint round-trips through a {type, params} payload: an instance becomes a payload of its registered class name and constructor parameter values, and the payload becomes an instance that solves identically. This is what lets an optimised index be cached, and it is the same shape the server uses for constraint rows.

Only constraint classes registered in the catalogue can be serialised. An unregistered class is refused, never guessed at.

:class:OptimisationConfig is the object form of the same description: what an optimised run is asked to do, held together so Backtest.run can take one argument rather than a growing list of them.

OptimisationConfig dataclass

OptimisationConfig(
    objective: str = MIN_TRACKING_ERROR,
    constraints: Sequence[Constraint] = (),
    risk_model: RiskModel | None = None,
)

What an optimised run is asked to do.

Attributes:

Name Type Description
objective str

What to minimise. Only "min_tracking_error" exists today, and the field names it anyway so a stored config says what it meant when other objectives arrive.

constraints Sequence[Constraint]

What the answer must satisfy, as :class:~beacon.optimise.constraints.Constraint instances. Empty means the solver's own default of full investment alone.

risk_model RiskModel | None

Reserved: carried but unused. The slot exists so covariance-aware optimised runs can arrive without changing this shape. Nothing reads it yet and passing one changes no result, though an optimised index carrying one cannot be cached.

constraint_payload

constraint_payload(
    constraint: Constraint,
) -> dict[str, Any]

One constraint as its registered name plus parameter values.

Parameter names come from the catalogue's constructor introspection, each value read off the instance attribute of the same name. Every registered type follows that convention, and the cache fingerprint keys by it.

Parameters:

Name Type Description Default
constraint Constraint

The instance to describe.

required

Returns:

Name Type Description
dict dict[str, Any]

{"type": class name, "params": {name: value}}, with every

dict[str, Any]

value JSON-serialisable.

Raises:

Type Description
CalculationError

If the class is not registered under the CONSTRAINT kind, keeps no attribute for one of its constructor parameters, or holds a value JSON cannot carry. Refused rather than guessed at: a payload nothing can rebuild is worse than no payload.

constraint_from_payload

constraint_from_payload(
    payload: Mapping[str, Any],
) -> Constraint

Rebuild a constraint from a {type, params} payload.

The inverse of :func:constraint_payload, and the builder the server's constraint rows go through: one code path from a stored document to the object the solver receives.

Parameters:

Name Type Description Default
payload Mapping[str, Any]

A mapping carrying type and, optionally, params.

required

Returns:

Name Type Description
Constraint Constraint

A fresh instance that solves identically to the one the

Constraint

payload was taken from.

Raises:

Type Description
CalculationError

If the type names nothing registered. A constructor rejecting the params (a missing argument, a minimum above a maximum) propagates untouched, so the class's own message reaches the caller.

constraints

Constraints an optimisation must respect.

Each class maps to one row a user adds in a constraint editor (position bounds, sector bounds, a turnover budget, a holding count, full investment), so the client can build a problem without translating between two vocabularies.

Every constraint states itself once, as :class:Condition objects in scipy's own convention: an equality holds when its function is zero, an inequality when its function is non-negative. That single statement is then used three ways: it is handed to the solver, it decides which constraints came out binding, and it verifies the returned weights actually satisfy what was asked. The three can therefore never drift apart, which is the point: the verification pass exists to catch a solver that returned something it should not have, and it would be worthless if it checked a second, separately-written copy of the rules.

Condition dataclass

Condition(
    label: str,
    kind: str,
    evaluate: Callable[[Vector], float],
    gradient: Callable[[Vector], Vector] | None = None,
)

One scalar condition on a weight vector.

Attributes:

Name Type Description
label str

Human-readable description, carried through to the binding report so a client can show which rule bit.

kind str

EQUALITY or INEQUALITY.

evaluate Callable[[Vector], float]

The condition function. Zero when an equality holds, non-negative when an inequality holds.

gradient Callable[[Vector], Vector] | None

Derivative with respect to the weights, when one can be written down. Supplying it keeps the solver off finite differences, which on a problem this small is most of the accuracy.

Slack dataclass

Slack(
    label: str,
    kind: str,
    slack: float,
    unit: str = FRACTION,
)

How much room a condition has left at a given solution.

Attributes:

Name Type Description
label str

The condition's description.

kind str

EQUALITY or INEQUALITY.

slack float

Signed room. For an inequality this is the condition function itself, so zero means the solution sits exactly on the boundary and negative means it has crossed. For an equality it is the negated absolute residual, which makes an exactly-satisfied equality read as zero slack, correctly, since an equality is always binding.

unit str

What slack is measured in, :data:FRACTION or :data:COUNT, copied off the constraint that produced it. Carried because a number whose unit is only knowable from the constraint's class name is a number no consumer can format.

is_binding property
is_binding: bool

Whether the solution sits on this condition's boundary.

is_violated property
is_violated: bool

Whether the solution breaks this condition.

Constraint

Bases: ABC

Something an optimal weight vector must satisfy.

Subclasses state their rules as conditions, and everything else (solving, binding detection, verification) is derived from those.

Attributes:

Name Type Description
UNIT str

What this constraint's slack is measured in. :data:FRACTION for everything expressed as a share of something (weights, turnover, an expected return), which is every constraint here but one, so it is the default a new subclass inherits. Override it when the quantity is not a fraction; :class:Cardinality counts names.

conditions abstractmethod
conditions(assets: Sequence[str]) -> list[Condition]

The conditions this constraint imposes over assets, in order.

Parameters:

Name Type Description Default
assets Sequence[str]

The universe, fixing the meaning of each weight position.

required

Returns:

Name Type Description
list list[Condition]

Conditions in scipy's convention.

solver_conditions
solver_conditions(assets: Sequence[str]) -> list[Condition]

The subset of the conditions that should go to the solver.

Everything by default. A constraint overrides this to nothing when it reaches the solver some other way (as a box, or by restricting the problem) while still reporting its conditions for verification.

bounds
bounds(
    assets: Sequence[str],
) -> list[tuple[float, float]] | None

Per-asset box limits, when this constraint is one.

A box handed to the solver as a bound is enforced at every iterate, whereas the same box as an inequality is only approached from outside; for position limits that difference is worth the special case.

Returns:

Type Description
list[tuple[float, float]] | None

list or None: One (low, high) per asset, or None if this

list[tuple[float, float]] | None

constraint is not a box.

report
report(
    weights: Vector, assets: Sequence[str]
) -> list[Slack]

How much room this constraint has left at weights.

Parameters:

Name Type Description Default
weights Vector

A candidate solution, aligned to assets.

required
assets Sequence[str]

The universe.

required

Returns:

Name Type Description
list list[Slack]

One Slack per condition, each stamped with this constraint's

list[Slack]

attr:UNIT.

validate
validate(assets: Sequence[str]) -> None

Raise if this constraint cannot be applied to assets at all.

Separate from feasibility: this catches a constraint that is malformed or refers to names that are not there, which is a caller mistake rather than an over-tight problem. It does nothing by default, and a subclass overrides it only when it has something to check.

FullInvestment

FullInvestment(target: float = 1.0)

Bases: Constraint

The weights must sum to a fixed total, normally one.

Attributes:

Name Type Description
target

The total. One means fully invested with no leverage and no cash; below one leaves cash, above one is levered.

PositionBounds

PositionBounds(
    minimum: float = 0.0,
    maximum: float = 1.0,
    assets: Sequence[str] | None = None,
)

Bases: Constraint

Lower and upper limits on individual weights.

Attributes:

Name Type Description
minimum

Smallest weight any covered asset may take. Zero forbids short positions, which is the usual index-tracking case.

maximum

Largest weight any covered asset may take.

assets

Names this applies to, or None for every name. Several of these compose (a blanket rule plus a tighter one on a few names), and the tightest limit on each name wins.

validate
validate(assets: Sequence[str]) -> None

Reject a bound on a name that is not in the universe.

Unlike a group, a position bound names one asset explicitly, so a name that is not there is a typo rather than a broader definition being reused, and silently dropping it would return an answer that ignored a limit the caller asked for.

bounds
bounds(assets: Sequence[str]) -> list[tuple[float, float]]

The box, unlimited on any asset this constraint does not cover.

solver_conditions
solver_conditions(assets: Sequence[str]) -> list[Condition]

None: the solver receives this constraint as a box instead.

conditions
conditions(assets: Sequence[str]) -> list[Condition]

The same box, written as inequalities.

Not given to the solver (:meth:bounds covers that), but used to report which positions came out at a limit.

GroupBounds

GroupBounds(
    name: str,
    members: Sequence[str],
    minimum: float = 0.0,
    maximum: float = 1.0,
)

Bases: Constraint

Limits on the combined weight of a set of names.

A sector, a country, a liquidity bucket: anything the client groups by.

Attributes:

Name Type Description
name

What the group is, used in the binding report.

members

The names in it.

minimum

Smallest combined weight the group may take.

maximum

Largest combined weight the group may take.

validate
validate(assets: Sequence[str]) -> None

Reject a group with no members in the universe.

Members outside the universe are dropped rather than rejected: a sector map is defined over a whole market and is meant to be reused across indices, so naming companies this index does not hold is normal. A group that matches nothing, though, is a mistake: it would silently constrain an empty sum, which is always satisfied.

TurnoverBudget

TurnoverBudget(
    maximum: float, current_weights: dict[str, float]
)

Bases: Constraint

A limit on how far the solution may move from the current holdings.

Turnover here is one-way: half the sum of absolute weight changes, which under full investment is the amount bought, and equally the amount sold. A budget of 5% therefore means what an index methodology means by "5% turnover", not a 2.5% round trip.

Attributes:

Name Type Description
maximum

The one-way budget, as a fraction of the portfolio.

current_weights

Where the portfolio is now. Names absent from it are treated as currently unheld.

turnover
turnover(weights: Vector, assets: Sequence[str]) -> float

One-way turnover of weights against the current holdings.

conditions
conditions(assets: Sequence[str]) -> list[Condition]

The budget as one inequality.

The absolute value has a kink wherever an asset's weight equals its current weight, so this function is not differentiable everywhere and the gradient used is a subgradient: the sign vector, which is the true derivative away from those kinks and an arbitrary choice of one at them. SLSQP assumes smoothness and can in principle stall on a solution that sits exactly on many kinks at once. In practice it converges, and the verification pass refuses the answer if it does not: an exact formulation needs auxiliary variables for the positive and negative parts, which doubles the problem and is not worth it until a real case demands it.

ExpectedReturnTarget

ExpectedReturnTarget(
    expected_returns: dict[str, float], target: float
)

Bases: Constraint

The portfolio must be expected to return exactly this much.

The constraint that traces out a frontier: fix the return, minimise the variance, repeat. It is an equality rather than a floor because a frontier point is a specific point, and a floor would let every solve collapse onto the minimum-variance portfolio whenever that portfolio happened to clear the bar.

Attributes:

Name Type Description
expected_returns

Expected return per asset, in the same units and over the same horizon as the risk model's covariance. Annualised, if the risk model is.

target

The portfolio return being asked for.

validate
validate(assets: Sequence[str]) -> None

Every asset must have an expected return.

Treating an absent one as zero would quietly bias the whole frontier towards holding it, since a zero-return asset looks like the safest way to satisfy a low return target.

Cardinality

Cardinality(maximum: int)

Bases: Constraint

A limit on how many names may be held.

Unlike every other constraint here this one is not convex: it counts non-zero positions, and no continuous solver can express that. It is honoured by a two-stage heuristic (see :func:beacon.optimise.solver.minimise_tracking_error): solve, keep the largest positions, then solve again with the rest pinned at zero. The answer satisfies the limit but is not proven optimal, and the exact problem is a mixed-integer program that would need a different solver entirely.

Attributes:

Name Type Description
maximum

The largest number of names that may carry weight.

solver_conditions
solver_conditions(assets: Sequence[str]) -> list[Condition]

None: counting holdings is a step function.

Its gradient is zero everywhere it is defined, so handing it to SLSQP would say that dropping a name costs nothing and changes nothing. The limit is enforced by restricting the problem instead.

conditions
conditions(assets: Sequence[str]) -> list[Condition]

The count, as an inequality that is only ever checked.

count_holdings

count_holdings(weights: Vector) -> int

How many positions carry meaningful weight.

one_way_turnover

one_way_turnover(weights: Vector, current: Vector) -> float

Half the summed absolute weight change between two portfolios.

The halving is what makes this one-way: under full investment every unit bought is a unit sold, so the undivided sum counts each trade twice.

frontier

The efficient frontier, and the two points on it worth naming.

A frontier is not one optimisation but a sequence of them: fix the expected return, minimise the variance, repeat across a grid. Each point answers "if I insist on earning this much, what is the least risk I can take to do it", and the curve through them is the boundary of what is achievable.

Two points on that curve are singled out because they answer questions people actually ask:

  • the minimum-variance portfolio: the least risky feasible portfolio, ignoring return entirely. It is the frontier's left-hand end, and the reason the grid starts there: portfolios with lower return than this exist, but each is beaten by one on the frontier with the same risk and more return.
  • the tangency portfolio: the highest Sharpe ratio available, the point where a line from the risk-free rate first touches the frontier.
What constraints do to this

Textbook frontiers are drawn with only a budget constraint, which admits a closed form. Every real mandate has more than that, and once position bounds and group limits are in play there is no closed form and the curve has to be traced numerically. Two consequences worth stating plainly:

The frontier can be shorter than the unconstrained one at both ends (a cap limits how much can be put into the highest-returning asset, so the right-hand end stops early), and it lies below it everywhere in between, because every constraint removes portfolios and can only make the best remaining one worse.

Maximising the Sharpe ratio is not a convex problem in general. The solve is warm-started from the best point on the grid, which in the long-only fully invested case is enough for the answer to be the global one; the grid is returned alongside so a caller can see the curve the tangency point sits on.

FrontierPoint dataclass

FrontierPoint(
    weights: Series,
    volatility: float,
    expected_return: float | None = None,
    sharpe_ratio: float | None = None,
    binding: list[str] = list(),
    heuristic: bool = False,
)

One portfolio on the frontier.

Attributes:

Name Type Description
weights Series

The portfolio, indexed by asset id.

volatility float

Annualised standard deviation, in the risk model's units.

expected_return float | None

Portfolio expected return, or None when no expected returns were supplied, since a return cannot be reported if it was never given.

sharpe_ratio float | None

Excess return over volatility, or None when the expected return is unknown or the volatility is negligible.

binding list[str]

Labels of the constraints this point sits on. The interesting part of a frontier point: it says which rule is what stops the portfolio from doing better.

heuristic bool

Whether a non-convex constraint forced a restricted re-solve, so this point is feasible but not proven optimal.

EfficientFrontier dataclass

EfficientFrontier(
    points: list[FrontierPoint],
    minimum_variance: FrontierPoint,
    tangency: FrontierPoint,
    risk_free_rate: float = 0.0,
)

A traced frontier and the points on it worth naming.

Attributes:

Name Type Description
points list[FrontierPoint]

The grid, in increasing order of expected return. The first is the minimum-variance portfolio and the last is the highest return the constraints allow.

minimum_variance FrontierPoint

The least risky feasible portfolio.

tangency FrontierPoint

The highest Sharpe ratio available.

risk_free_rate float

The rate the Sharpe ratios were computed against.

volatilities property
volatilities: list[float]

Each point's volatility, in grid order.

expected_returns property
expected_returns: list[float | None]

Each point's expected return, in grid order.

is_monotonic
is_monotonic(tolerance: float = 1e-07) -> bool

Whether risk rises with return across the grid.

The defining property of a frontier. It should always hold (insisting on more return can only cost risk), so a False here means a point failed to solve to optimality rather than that the curve is unusual.

to_frame
to_frame() -> pd.DataFrame

The frontier as a table, one row per point.

weights_frame
weights_frame() -> pd.DataFrame

Every point's weights, points on the index and assets on the columns.

default_constraints

default_constraints() -> list[Constraint]

Long-only and fully invested.

The frontier's default rather than full investment alone, because with shorting unbounded the maximum-return portfolio does not exist: short the worst asset without limit to fund the best, and the return grows forever. A frontier needs a right-hand end.

minimum_variance_portfolio

minimum_variance_portfolio(
    risk_model: RiskModel,
    constraints: Sequence[Constraint] | None = None,
    expected_returns: dict[str, float] | None = None,
    risk_free_rate: float = 0.0,
) -> FrontierPoint

The least risky feasible portfolio.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance to minimise against.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means long-only and fully invested.

None
expected_returns dict[str, float] | None

Used only to report the point's return and Sharpe ratio; it has no effect on the weights, since minimum variance ignores return by definition.

None
risk_free_rate float

For the reported Sharpe ratio.

0.0

Returns:

Name Type Description
FrontierPoint FrontierPoint

The portfolio, its risk, and which constraints bound.

Raises:

Type Description
CalculationError

If the constraints cannot be satisfied.

maximum_return_portfolio

maximum_return_portfolio(
    risk_model: RiskModel,
    expected_returns: dict[str, float],
    constraints: Sequence[Constraint] | None = None,
    risk_free_rate: float = 0.0,
) -> FrontierPoint

The highest-returning feasible portfolio.

The frontier's right-hand end. Maximising a linear objective often has many optimal solutions (any mix of the top assets, if they tie), so the return is found first and the variance minimised subject to achieving it, which picks the sensible one out of the tie.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance, used to break ties and report risk.

required
expected_returns dict[str, float]

Expected return per asset.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means long-only and fully invested.

None
risk_free_rate float

For the reported Sharpe ratio.

0.0

Returns:

Name Type Description
FrontierPoint FrontierPoint

The portfolio.

efficient_frontier

efficient_frontier(
    risk_model: RiskModel,
    expected_returns: dict[str, float],
    points: int = DEFAULT_POINTS,
    constraints: Sequence[Constraint] | None = None,
    risk_free_rate: float = 0.0,
) -> EfficientFrontier

Trace the frontier, and locate its minimum-variance and tangency points.

Parameters:

Name Type Description Default
risk_model RiskModel

The covariance. Its asset order defines the universe.

required
expected_returns dict[str, float]

Expected return per asset, in the same units and over the same horizon as the covariance: annualised, if it is.

required
points int

How many portfolios to solve for, minimum 2.

DEFAULT_POINTS
constraints Sequence[Constraint] | None

What every point must satisfy. None means long-only and fully invested.

None
risk_free_rate float

The rate Sharpe ratios are measured against.

0.0

Returns:

Name Type Description
EfficientFrontier EfficientFrontier

The grid and the two named points.

Raises:

Type Description
CalculationError

If points is below 2, if the constraints cannot be satisfied, or if the problem is unbounded above.

result

OptimisationResult: the output of a solve.

Same shape as IndexResult, BacktestResult and RiskModel: a dataclass of pandas structures with accessors that answer the questions a caller actually has, rather than handing back a bare weight vector and leaving everyone to recompute the active position and the turnover themselves.

BindingConstraint dataclass

BindingConstraint(
    label: str,
    kind: str,
    slack: float,
    unit: str = FRACTION,
)

A constraint the solution sits exactly on.

Binding constraints are the interesting part of an answer: they are the rules that actually cost something, and relaxing one of them is the only way to improve the objective.

Attributes:

Name Type Description
label str

What the constraint was.

kind str

EQUALITY or INEQUALITY.

slack float

Room left, at or near zero by definition of binding. Carried so a caller can see how tight "tight" was.

unit str

What slack is measured in, off the constraint that produced it. Near-meaningless here, where the number is zero by construction; it matters on the non-binding slacks in :attr:OptimisationResult .slacks, and the two carry the same field so a client formats either the same way.

SolverDiagnostics dataclass

SolverDiagnostics(
    converged: bool,
    iterations: int,
    evaluations: int,
    objective: float,
    status: int,
    message: str,
)

What the solver did.

Attributes:

Name Type Description
converged bool

Whether the solver reported success. An answer is only returned when this is True and the weights pass verification, so a caller seeing this is seeing a solve that worked.

iterations int

Major iterations taken.

evaluations int

Objective evaluations.

objective float

Final objective value. It is a variance, so the tracking error is its square root.

status int

The solver's numeric exit code.

message str

The solver's own description of how it exited.

OptimisationResult dataclass

OptimisationResult(
    weights: Series,
    target_weights: Series,
    binding: list[BindingConstraint],
    diagnostics: SolverDiagnostics,
    slacks: list[Slack] = list(),
    heuristic: bool = False,
    _risk_model: RiskModel | None = None,
)

Optimal weights, what they cost, and which rules bound.

Attributes:

Name Type Description
weights Series

The solution, indexed by asset id.

target_weights Series

What the solve was tracking, on the same index.

binding list[BindingConstraint]

Constraints the solution sits on, tightest first.

diagnostics SolverDiagnostics

How the solve went.

slacks list[Slack]

Every constraint's room at the solution, binding or not. Kept so a caller can see what nearly bound as well as what did.

heuristic bool

Whether a non-convex constraint forced a heuristic stage, in which case the answer satisfies every constraint but is not proven optimal.

asset_ids property
asset_ids: list[str]

Assets in the solution, in weight-vector order.

active_weights property
active_weights: Series

Solution minus target: the active position.

Sums to zero whenever both sides are fully invested to the same total, which is the usual case and worth checking: a non-zero sum means the solve was allowed to change how much is invested, not just where.

holdings property
holdings: int

How many names carry meaningful weight.

with_risk_model
with_risk_model(
    risk_model: RiskModel,
) -> OptimisationResult

Bind a risk model for risk-based accessors. Returns self.

tracking_error
tracking_error() -> float

Distance from the target, in the metric the solve minimised.

With a risk model this is annualised tracking error, the quantity a tracking mandate is measured on. Without one the objective's identity covariance makes it the Euclidean distance between the two weight vectors: a sensible thing to minimise, but not a volatility, and not comparable to a number produced with a risk model.

turnover
turnover(
    current_weights: dict[str, float] | None = None,
) -> float

One-way turnover from current_weights to the solution.

Half the summed absolute weight change, matching :class:~beacon.optimise.constraints.TurnoverBudget. Defaults to measuring against the target, which answers "how far did the optimiser move me off the index".

Parameters:

Name Type Description Default
current_weights dict[str, float] | None

Where the portfolio is now. None measures against the target weights.

None

Returns:

Name Type Description
float float

One-way turnover as a fraction of the portfolio.

to_frame
to_frame() -> pd.DataFrame

Target, optimal and active weights side by side, largest active first.

binding_labels
binding_labels() -> list[str]

Just the descriptions of the binding constraints.

solver

Constrained minimisation, and the tracking-error objective built on it.

:func:solve_constrained is the objective-agnostic core: give it something to minimise and a list of constraints and it handles the box, the infeasibility messages, the non-convex cardinality stage and the verification pass. The frontier objectives in beacon.optimise.frontier use the same core, so the guarantees below hold for all of them.

The first objective an index business needs is not mean-variance: it is "get me as close as possible to this target, subject to what I am allowed to hold". That is what this solves: minimise

(w - b)ᵀ Σ (w - b)

over the weights w, given a target b and whatever constraints were supplied. With a risk model, Σ is its covariance and the objective is squared tracking error. Without one, Σ is the identity and the objective is the squared distance between the two weight vectors, which is the same problem with every asset treated as equally risky and uncorrelated. The two share a code path because they are the same problem; only the metric differs.

The objective is convex (Σ is positive semi-definite by construction), and every constraint but cardinality is convex (linear, apart from the turnover budget), so a local optimum is the global one and SLSQP is an appropriate solver.

Refusing rather than fudging

A solve either returns an answer that satisfies every constraint or it raises. There is no third outcome where a violating vector comes back with a warning attached, because a weight vector is the kind of thing a caller will act on and an ignored warning becomes a breached mandate. Two checks enforce that: cheap provable-infeasibility tests before solving, which produce a message naming what is impossible rather than an opaque solver code, and a verification pass afterwards that re-evaluates every constraint against the returned weights.

Non-convergence raises for the same reason. A stalled solve leaves a feasible but not-necessarily-optimal point, and returning it with converged=False in the diagnostics would make "this is the best answer" and "this is merely an answer" look identical to anyone who does not check the flag.

Solution dataclass

Solution(
    weights: Vector,
    outcome: Any,
    slacks: list[Slack],
    heuristic: bool,
)

A verified answer to a constrained problem.

Attributes:

Name Type Description
weights Vector

The solution vector, aligned to the universe.

outcome Any

The solver's own result object.

slacks list[Slack]

Every constraint's room at the solution.

heuristic bool

Whether a non-convex constraint forced a restricted re-solve, in which case the answer is feasible but not proven optimal.

minimise_tracking_error

minimise_tracking_error(
    target_weights: Series | dict[str, float],
    constraints: Sequence[Constraint] | None = None,
    risk_model: RiskModel | None = None,
) -> OptimisationResult

Find the closest feasible portfolio to a target.

Parameters:

Name Type Description Default
target_weights Series | dict[str, float]

What to track, by asset id. Defines the universe: the optimiser allocates over exactly these names, in this order.

required
constraints Sequence[Constraint] | None

What the answer must satisfy. None means full investment alone, which is the smallest problem that has a unique answer.

None
risk_model RiskModel | None

Covariance to measure distance with. None treats every asset as equally risky and uncorrelated, which minimises plain squared weight distance.

None

Returns:

Name Type Description
OptimisationResult OptimisationResult

Optimal weights, the active position, which

OptimisationResult

constraints bound, and how the solve went.

Raises:

Type Description
CalculationError

If the constraints cannot all be satisfied, if the solver fails to converge, or if the returned weights violate a constraint. Also if a constraint or the risk model refers to assets outside the universe.

solve_constrained

solve_constrained(
    objective: Callable[[Vector], float],
    gradient: Callable[[Vector], Vector],
    rules: Sequence[Constraint],
    assets: Sequence[str],
    hint: Vector | None = None,
) -> Solution

Minimise objective over the weights, subject to rules.

The objective-agnostic core. Tracking error is one objective; portfolio variance, expected return and the Sharpe ratio are others, and all of them want the same constraint handling, the same infeasibility messages and the same refusal to return a violating answer.

Parameters:

Name Type Description Default
objective Callable[[Vector], float]

What to minimise, as a function of the weight vector.

required
gradient Callable[[Vector], Vector]

Its derivative. Required rather than optional: finite differences on a problem this small cost more accuracy than they save effort.

required
rules Sequence[Constraint]

The constraints.

required
assets Sequence[str]

The universe, fixing the meaning of each weight position.

required
hint Vector | None

Where to start the search. None starts from equal weights.

None

Returns:

Name Type Description
Solution Solution

The verified answer.

Raises:

Type Description
CalculationError

If the constraints cannot all be satisfied, if the solver fails to converge, or if the weights violate a constraint.

covariance_matrix

covariance_matrix(
    risk_model: RiskModel | None, assets: Sequence[str]
) -> Vector

The metric the objective measures distance in.

The identity is not a placeholder standing in for a missing model: it is the honest statement that without one, every unit of active weight is equally costly wherever it is taken.

weight_box

weight_box(
    rules: Sequence[Constraint], assets: Sequence[str]
) -> tuple[Vector, Vector]

Intersect every position-bound constraint into one box.

Several bounds may cover the same asset (a blanket rule plus a tighter one on a few names), and the answer must satisfy all of them, so the tightest limit on each side wins.

Returns:

Name Type Description
tuple Vector

The lower and upper bound per asset, aligned to assets.

Vector

Unbounded sides are infinite.