markowitz¶
markowitz
¶
markowitz-optimizer -- research-grade mean-variance portfolio toolkit.
Public entry point. Exposes the installed package version via
:pydata:markowitz.__version__ and re-exports the headline public API so
that from markowitz import MeanVariance works without users having to
remember submodule paths.
AbsoluteView(asset: str, expected_return: float, confidence: float | None = None)
dataclass
¶
A point estimate on a single asset's expected excess return.
AnalyticFrontier(mu: npt.ArrayLike, Sigma: npt.ArrayLike)
¶
Closed-form mean-variance frontier for (mu, Sigma).
Construction is the only point at which Sigma is factored; all
subsequent queries (gmv, tangency, efficient_return,
frontier) are O(n) once the cached basis vectors are available.
Source code in src/markowitz/core/frontier.py
abcd: MertonABCD
property
¶
The Merton scalars associated with (mu, Sigma).
is_degenerate: bool
property
¶
True when D is essentially zero.
A degenerate frontier collapses to a single point in mean-
variance space because mu is collinear with 1 (every
asset has the same expected return). Efficient-return queries
are still well-defined at the GMV expected return but are
infeasible for any other target.
n_assets: int
property
¶
Number of assets in the investment universe.
efficient_return(mu_p: float) -> Portfolio
¶
Return the minimum-variance portfolio with expected return mu_p.
Source code in src/markowitz/core/frontier.py
frontier(n_points: int = 100, *, mu_min: float | None = None, mu_max: float | None = None) -> list[Portfolio]
¶
Return n_points portfolios spanning [mu_min, mu_max].
Defaults straddle the GMV expected return symmetrically: if no
bounds are supplied, the frontier extends from mu_GMV to
mu_GMV + 2 * (max(mu) - mu_GMV). Callers that need a
specific range should pass it explicitly.
Source code in src/markowitz/core/frontier.py
gmv() -> Portfolio
¶
Return the global minimum-variance portfolio.
Source code in src/markowitz/core/frontier.py
tangency(rf: float) -> Portfolio
¶
Return the tangency portfolio for risk-free rate rf.
Raises:
| Type | Description |
|---|---|
InfeasibleFrontierError
|
If |
Source code in src/markowitz/core/frontier.py
BlackLitterman(cov: pd.DataFrame, market_weights: pd.Series, views: Views | None = None, *, tau: float = 0.05, delta: float = 2.5, omega: np.ndarray | None = None, omega_method: str = 'he_litterman', posterior_covariance_mode: str = 'full', risk_free_rate: float | None = None)
¶
Construct a Black-Litterman posterior from a covariance prior and views.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov
|
DataFrame
|
Prior covariance matrix as a labelled |
required |
market_weights
|
Series
|
Market-cap weights series aligned to the covariance index. |
required |
views
|
Views | None
|
:class: |
None
|
tau
|
float
|
Prior-scaling parameter. Must be strictly positive. Defaults to
|
0.05
|
delta
|
float
|
Representative-investor risk-aversion coefficient. Defaults to
|
2.5
|
omega
|
ndarray | None
|
Optional pre-computed view-uncertainty matrix; if supplied,
|
None
|
omega_method
|
str
|
Strategy used to derive |
'he_litterman'
|
posterior_covariance_mode
|
str
|
|
'full'
|
risk_free_rate
|
float | None
|
Optional metadata used by :meth: |
None
|
Source code in src/markowitz/views/black_litterman.py
implied_returns() -> pd.Series
¶
Return the equilibrium-implied prior pi = delta * Sigma * w_mkt.
Source code in src/markowitz/views/black_litterman.py
posterior_covariance() -> pd.DataFrame
¶
Return the posterior covariance matrix.
Source code in src/markowitz/views/black_litterman.py
posterior_returns() -> pd.Series
¶
Return the Black-Litterman posterior excess-return vector.
posterior_weights(*, constrained: bool = False) -> pd.Series
¶
Return the unconstrained tangency weights implied by the posterior.
w_BL = (delta * Sigma_BL)^{-1} * mu_BL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constrained
|
bool
|
If |
False
|
Source code in src/markowitz/views/black_litterman.py
summary() -> pd.DataFrame
¶
Return a per-asset table with prior, posterior and weight tilts.
Source code in src/markowitz/views/black_litterman.py
CAPMReturns(*, risk_free_rate: float = 0.0, market_premium: float | None = None, annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseMean
CAPM-implied expected returns from per-asset OLS betas.
Source code in src/markowitz/estimators/means.py
CustomConstraint(builder: Callable[[cp.Variable, ConstraintContext], list[cp.Constraint]], description: str = 'custom')
dataclass
¶
Escape hatch for user-defined CVXPY constraints.
builder is a callable (w, ctx) -> list[cvxpy.Constraint]; it is
invoked freshly each solve so it should not close over solver state.
Source code in src/markowitz/optimizer/constraints.py
EWMACovariance(*, halflife_years: float | None = None, lam: float | None = None, burn_in: int | Literal['auto'] = 'auto', annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseCov
Exponentially weighted covariance (RiskMetrics recursion).
Source code in src/markowitz/estimators/covariance.py
EWMAMean(*, halflife_years: float | None = None, lam: float | None = None, annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseMean
Exponentially weighted mean using RiskMetrics-style decay.
Source code in src/markowitz/estimators/means.py
ImpliedReturns(*, delta: float = 2.5)
¶
InfeasibleError(message: str, *, solver_status: str | None = None, solver_message: str | None = None, constraints_violated: list[str] | None = None)
¶
Bases: OptimizationError
Raised when the feasible region is empty.
Either the solver returned infeasible / infeasible_inaccurate,
or an upfront sanity check (e.g. max(mu) <= rf for max-Sharpe)
detected that no admissible portfolio exists.
Source code in src/markowitz/optimizer/exceptions.py
JorionBayesStein(*, base_cov: np.ndarray | None = None, annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseMean
Bayes-Stein shrinkage of the sample mean toward the minimum-variance mean.
Source code in src/markowitz/estimators/means.py
LedoitWolfShrinkage(*, target: Literal['identity', 'constant_corr'] = 'identity', annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseCov
Ledoit-Wolf shrinkage covariance, sklearn-compatible for the identity target.
Source code in src/markowitz/estimators/covariance.py
LeverageCap(max_leverage: float)
dataclass
¶
Gross-exposure cap sum(|w_i|) <= max_leverage.
LongOnly()
dataclass
¶
No short sales: w_i >= 0 for every asset.
MeanVariance(mu: pd.Series | np.ndarray, sigma: pd.DataFrame | np.ndarray, weight_bounds: tuple[float | None, float | None] = (0.0, 1.0), *, solver: str = 'CLARABEL', solver_options: dict[str, Any] | None = None, regularize: float = _REGULARIZE_DEFAULT)
¶
Convex mean-variance optimizer over a single horizon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Series | ndarray
|
Expected returns; either a |
required |
sigma
|
DataFrame | ndarray
|
Covariance matrix; either a |
required |
weight_bounds
|
tuple[float | None, float | None]
|
Default |
(0.0, 1.0)
|
solver
|
str
|
CVXPY solver name forwarded to :func: |
'CLARABEL'
|
solver_options
|
dict[str, Any] | None
|
Extra keyword arguments forwarded to the solver backend. |
None
|
regularize
|
float
|
Ridge term added to |
_REGULARIZE_DEFAULT
|
Source code in src/markowitz/optimizer/mean_variance.py
add_constraint(constraint: Constraint) -> MeanVariance
¶
clear_constraints() -> MeanVariance
¶
efficient_return(target_return: float) -> pd.Series
¶
Minimise variance subject to mu^T w >= target_return.
Source code in src/markowitz/optimizer/mean_variance.py
efficient_risk(target_volatility: float) -> pd.Series
¶
Maximise mu^T w subject to sqrt(w^T Sigma w) <= target_vol.
Source code in src/markowitz/optimizer/mean_variance.py
max_quadratic_utility(risk_aversion: float = 1.0) -> pd.Series
¶
Maximise mu^T w - (lambda/2) w^T Sigma w.
With risk_aversion = lambda. lambda > 0 is required.
Source code in src/markowitz/optimizer/mean_variance.py
max_sharpe(risk_free_rate: float = 0.0) -> pd.Series
¶
Maximise the Sharpe ratio via the Cornuejols--Tutuncu reformulation.
Source code in src/markowitz/optimizer/mean_variance.py
min_volatility() -> pd.Series
¶
Minimise w^T Sigma w subject to sum(w) = 1 and constraints.
Source code in src/markowitz/optimizer/mean_variance.py
portfolio_performance(weights: pd.Series | np.ndarray | None = None, *, risk_free_rate: float = 0.0) -> tuple[float, float, float]
¶
Return (expected_return, volatility, sharpe_ratio).
If weights is None the most-recently-computed weights are
used; an error is raised when no solve has happened yet.
Source code in src/markowitz/optimizer/mean_variance.py
OneOverN
¶
Equal-weight benchmark of DeMiguel, Garlappi, Uppal (2009).
OptimizationError(message: str, *, solver_status: str | None = None, solver_message: str | None = None, constraints_violated: list[str] | None = None)
¶
Bases: Exception
Base class for any failure originating in the optimizer layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
Human-readable description of what went wrong. |
required |
solver_status
|
str | None
|
The raw status string returned by the underlying solver (e.g.
|
None
|
solver_message
|
str | None
|
Free-form additional message produced by the solver, when available. |
None
|
constraints_violated
|
list[str] | None
|
Names of the constraints that the post-solve verification step flagged as violated, when the error was raised after a solve. |
None
|
Source code in src/markowitz/optimizer/exceptions.py
Portfolio(weights: FloatArray, expected_return: float, volatility: float, sharpe: float | None = None)
dataclass
¶
A snapshot of a portfolio's weights and analytic performance.
Attributes:
| Name | Type | Description |
|---|---|---|
weights |
FloatArray
|
Length- |
expected_return |
float
|
|
volatility |
float
|
|
sharpe |
float | None
|
Sharpe ratio relative to whatever risk-free rate the caller
chose; |
RelativeView(long_leg: Mapping[str, float], short_leg: Mapping[str, float], spread: float, confidence: float | None = None)
dataclass
¶
A spread view between a long and a short basket of assets.
The basket weights specify how each leg is constructed: long_leg maps
asset names to non-negative coefficients and likewise short_leg. The
resulting pick row is long_leg - short_leg and must sum to zero (this
is verified at construction time).
SampleCovariance(*, annualize: bool = True, periods_per_year: int | None = None, ddof: int = 1)
¶
Bases: _BaseCov
Plain sample covariance np.cov(returns.T, ddof=ddof).
Source code in src/markowitz/estimators/covariance.py
SampleMean(*, annualize: bool = True, periods_per_year: int | None = None)
¶
Bases: _BaseMean
Plain sample mean μ̂ = mean(returns, axis=0).
Source code in src/markowitz/estimators/means.py
SectorCap(sector_mapping: Mapping[str, str], caps: Mapping[str, float], floors: Mapping[str, float] | None = None)
dataclass
¶
Per-sector exposure caps (and optional floors).
sector_mapping maps each ticker to the name of its sector; caps
(and optionally floors) map sector names to scalar weight bounds.
Tickers not mentioned in the mapping are simply ignored from the
grouping (treated as their own singleton "sector").
SolverError(message: str, *, solver_status: str | None = None, solver_message: str | None = None, constraints_violated: list[str] | None = None)
¶
Bases: OptimizationError
Raised when the solver fails for a reason other than (in)feasibility.
Examples include solver-internal numerical breakdowns, license errors, or a status string the optimizer does not recognise.
Source code in src/markowitz/optimizer/exceptions.py
TurnoverCap(prev_weights: pd.Series | np.ndarray, max_turnover: float)
dataclass
¶
L1 turnover budget against a previous-period weight vector.
sum(|w - prev|) <= max_turnover.
UnboundedError(message: str, *, solver_status: str | None = None, solver_message: str | None = None, constraints_violated: list[str] | None = None)
¶
Bases: OptimizationError
Raised when the solver reports the objective is unbounded.
This typically signals a missing leverage / box constraint on a problem that permits arbitrarily large weights.
Source code in src/markowitz/optimizer/exceptions.py
Views(views: Sequence[ViewSpec], assets: Sequence[str])
¶
Container that validates and serialises a list of :class:ViewSpec.
Source code in src/markowitz/views/view_specs.py
build_omega_he_litterman(tau: float, cov: np.ndarray) -> np.ndarray
¶
Return the He-Litterman diagonal Omega = diag(P tau Sigma P^T).
Source code in src/markowitz/views/view_specs.py
build_pq() -> tuple[np.ndarray, np.ndarray]
¶
Return the (P, Q) matrices for the current view set.
Source code in src/markowitz/views/view_specs.py
confidences() -> np.ndarray | None
¶
Return per-view confidences as an array, or None if unspecified.
Source code in src/markowitz/views/view_specs.py
WalkForward(returns: pd.DataFrame, strategies: Mapping[str, Strategy], *, rebalance: str = 'M', lookback: int = 120, rf: pd.Series | float | None = None, cost_bps: float = 10.0, debug_no_lookahead: bool = False)
¶
Rolling-window walk-forward backtester.
The engine slides a fixed-length lookback window across returns,
fits every supplied strategy on the strictly in-sample slice, and
holds the resulting weights until the next rebalance date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
DataFrame
|
Wide DataFrame of per-period simple returns indexed by date. |
required |
strategies
|
Mapping[str, Strategy]
|
Mapping |
required |
rebalance
|
str
|
Pandas offset alias. |
'M'
|
lookback
|
int
|
Length of the rolling estimation window, in periods. |
120
|
rf
|
Series | float | None
|
Risk-free rate; scalar or Series aligned to |
None
|
cost_bps
|
float
|
Proportional transaction cost in basis points per unit turnover. |
10.0
|
debug_no_lookahead
|
bool
|
If |
False
|
Source code in src/markowitz/backtest/walk_forward.py
run() -> BacktestResult
¶
Execute the walk-forward loop and return a :class:BacktestResult.
Source code in src/markowitz/backtest/walk_forward.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
WeightBounds(lower: float | Sequence[float], upper: float | Sequence[float])
dataclass
¶
Element-wise box constraint lower <= w_i <= upper.
The bounds may be supplied as scalars (broadcast across all assets) or
as iterables of length n_assets.
implied_returns(cov: pd.DataFrame, market_weights: pd.Series, delta: float = 2.5) -> pd.Series
¶
Compute the equilibrium-implied excess-return vector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov
|
DataFrame
|
Asset return covariance matrix as a square |
required |
market_weights
|
Series
|
Market-capitalisation weights indexed by asset. The index must align
with |
required |
delta
|
float
|
Representative-investor risk-aversion coefficient. Defaults to the
commonly cited value of |
2.5
|
Returns:
| Type | Description |
|---|---|
Series
|
Implied excess returns |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the covariance matrix is not square, weights cannot be aligned, or
|
Source code in src/markowitz/views/equilibrium.py
infer_delta(market_returns: pd.Series, risk_free_rate: float, ddof: int = 1) -> float
¶
Estimate the risk-aversion coefficient from market-return history.
Uses the standard identity delta = (E[r_m] - rf) / Var[r_m] where
r_m is the market portfolio return series. The series is assumed to
already be on the same periodicity as risk_free_rate (e.g. both annual
or both daily).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
market_returns
|
Series
|
Realised market returns (not excess). |
required |
risk_free_rate
|
float
|
Periodic risk-free rate matching the periodicity of
|
required |
ddof
|
int
|
Degrees-of-freedom passed through to :meth: |
1
|
Returns:
| Type | Description |
|---|---|
float
|
The implied risk-aversion coefficient. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the input series has fewer than two observations or has zero variance. |