optimization¶
Pure solver functions: (expected_returns, cov_matrix, constraints) -> weights. Every function here is importable directly, with no dependency on Portfolio - see construction for the discoverable, ModelResult-returning dispatcher over these (optimize/build), and estimators for computing expected_returns/cov_matrix.
solvers ¶
Pure portfolio optimization solvers: (expected_returns, cov_matrix, constraints) -> weights.
Every function here is importable directly for functional/advanced use - none of them depend on Portfolio or portpy.models.estimators, you bring your own mu/Sigma. All of them (except hierarchical_risk_parity, which needs no numerical solver) go through models.base.solve_weights - multi-start SLSQP; see that module's docstring for why PortPy stays scipy-only here instead of adding a convex-QP dependency like cvxpy.
Nothing here checks that expected_returns and cov_matrix share a time
scale. Every docstring below says "annualized" because that's what
portpy.models.estimators produces by default and what rf/risk_aversion
defaults assume, but the solvers themselves are scale-agnostic: pass a
periodic mu next to an annualized Sigma (or vice versa) and you get a
solvable, plausible-looking, and financially meaningless result - no
exception, no warning. Get both inputs from portpy.models.estimators (or
otherwise put both on the same footing yourself) before optimizing.
mean_variance ¶
mean_variance(
expected_returns: Series,
cov_matrix: DataFrame,
constraints: Sequence[Any] | None = None,
risk_aversion: float = 1.0,
) -> pd.Series
Classic Markowitz utility maximization: maximize w'mu - 0.5*risk_aversion*w'Sigma*w.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_returns
|
Series
|
Annualized expected return per asset. |
required |
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix (its columns define asset order). |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
risk_aversion
|
float
|
Higher values weight the variance penalty more heavily, pulling the solution toward lower risk (and lower return). |
1.0
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
min_variance ¶
Minimize portfolio variance: minimize w'Sigma*w. Needs no return forecast at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
max_sharpe ¶
max_sharpe(
expected_returns: Series,
cov_matrix: DataFrame,
constraints: Sequence[Any] | None = None,
rf: float = 0.0,
) -> pd.Series
Maximize the (annualized) Sharpe ratio: maximize (w'mu - rf) / sqrt(w'Sigma*w).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_returns
|
Series
|
Annualized expected return per asset. |
required |
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
rf
|
float
|
Annual risk-free rate, in the same (annualized) units as |
0.0
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
target_return ¶
target_return(
expected_returns: Series,
cov_matrix: DataFrame,
target: float,
constraints: Sequence[Any] | None = None,
) -> pd.Series
Minimize variance subject to hitting an exact expected return: w'mu == target.
Note this is an equality, not "at least target" - a target below the
global minimum-variance portfolio's own return still pins you to that
lower, needlessly higher-variance point (some other libraries, e.g.
PyPortfolioOpt's efficient_return, use a >= inequality instead, which
substitutes the min-variance portfolio once its own return already clears
the bar). The equality is what lets efficient_frontier sweep a full,
non-degenerate curve by calling this at every grid point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_returns
|
Series
|
Annualized expected return per asset. |
required |
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
target
|
float
|
Required annualized portfolio return. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Series
|
infeasible given the other constraints, the SLSQP solve won't fully |
Series
|
converge - check |
Source code in src/portpy/models/optimization/solvers.py
target_volatility ¶
target_volatility(
expected_returns: Series,
cov_matrix: DataFrame,
target: float,
constraints: Sequence[Any] | None = None,
) -> pd.Series
Maximize expected return subject to a volatility cap: w'Sigma*w <= target^2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_returns
|
Series
|
Annualized expected return per asset. |
required |
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
target
|
float
|
Maximum allowed annualized volatility. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Series
|
(volatility == target) whenever the unconstrained-by-risk optimum |
Series
|
would exceed it, which is the usual case. |
Source code in src/portpy/models/optimization/solvers.py
efficient_frontier ¶
efficient_frontier(
expected_returns: Series,
cov_matrix: DataFrame,
n_points: int = 50,
constraints: Sequence[Any] | None = None,
min_return: float | None = None,
max_return: float | None = None,
) -> pd.DataFrame
Trace the efficient frontier: the min-variance solution at each of n_points
target returns spanning [min_return, max_return].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_returns
|
Series
|
Annualized expected return per asset. |
required |
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
n_points
|
int
|
Number of frontier points to compute. |
50
|
constraints
|
Sequence[Any] | None
|
See |
None
|
min_return
|
float | None
|
Lower end of the return grid. Defaults to the global minimum-variance portfolio's own return. |
None
|
max_return
|
float | None
|
Upper end of the return grid. Defaults to the single
highest per-asset expected return (only exactly reachable
long-only/unconstrained; with custom bounds it may not be, in
which case that point's row is simply dropped - see below). This
default is a long-only ceiling: it does not account for leverage
or short-selling. If |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
A DataFrame with one row per (successfully solved) grid point: |
DataFrame
|
columns "target_return", "return", "volatility", "sharpe" (rf=0), |
DataFrame
|
plus one column per asset holding that point's weight. Points where |
DataFrame
|
|
DataFrame
|
are silently dropped, so the result can have fewer than |
Source code in src/portpy/models/optimization/solvers.py
black_litterman ¶
black_litterman(
cov_matrix: DataFrame,
market_weights: Series,
views: dict[str, float] | tuple[DataFrame, Series],
tau: float = 0.05,
risk_aversion: float = 2.5,
omega: DataFrame | None = None,
constraints: Sequence[Any] | None = None,
) -> pd.Series
Black-Litterman: blend market-implied equilibrium returns with explicit views, then mean-variance optimize the posterior estimates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
market_weights
|
Series
|
Market-capitalization (or other equilibrium) weights,
used to back out the implied equilibrium return |
required |
views
|
dict[str, float] | tuple[DataFrame, Series]
|
Either a simple absolute-view dict ( |
required |
tau
|
float
|
Scales the uncertainty of the equilibrium prior ( |
0.05
|
risk_aversion
|
float
|
Used both to back out |
2.5
|
omega
|
DataFrame | None
|
View uncertainty matrix. Defaults to He & Litterman's usual
proportional choice, |
None
|
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
hierarchical_risk_parity ¶
hierarchical_risk_parity(
cov_matrix: DataFrame,
constraints: Sequence[Any] | None = None,
linkage_method: str = "single",
) -> pd.Series
Hierarchical Risk Parity (Lopez de Prado, 2016): cluster assets by correlation, then recursively split inverse-variance weight down the tree instead of inverting the covariance matrix directly.
No numerical optimizer is involved - HRP is a deterministic, closed-form algorithm, which is also why it tolerates a poorly-conditioned (e.g. near-singular) covariance matrix far better than the matrix-inversion-based solvers above.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Asset covariance matrix (annualization doesn't affect the result - HRP only uses correlations and relative variances). |
required |
constraints
|
Sequence[Any] | None
|
Only |
None
|
linkage_method
|
str
|
|
'single'
|
Returns:
| Type | Description |
|---|---|
Series
|
Weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 | |
risk_parity ¶
Equal Risk Contribution: every asset contributes the same share of total
portfolio risk. A special case of risk_budgeting with an equal budget.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
risk_budgeting ¶
risk_budgeting(
cov_matrix: DataFrame,
budget: Series,
constraints: Sequence[Any] | None = None,
) -> pd.Series
Solve for weights whose risk contributions match a target budget:
weight_i * MCTR_i approx= budget_i * portfolio_variance for every asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
budget
|
Series
|
Target risk-contribution share per asset (renormalized to sum to 1 internally, so relative sizes are what matter). |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |
Source code in src/portpy/models/optimization/solvers.py
maximum_diversification ¶
maximum_diversification(
cov_matrix: DataFrame,
constraints: Sequence[Any] | None = None,
) -> pd.Series
Maximize metrics.covariance.diversification_ratio directly: maximize (w'sigma) / sqrt(w'Sigma*w).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_matrix
|
DataFrame
|
Annualized asset covariance matrix. |
required |
constraints
|
Sequence[Any] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Solved weights, indexed by |