Skip to content

What (One-liner)

Multi-asset Markowitz / risk-parity portfolio optimization. scipy SLSQP solves three objectives (max Sharpe / min variance / equal risk contribution) + a 20-point efficient frontier. Long-only, sum=1.

How to use

Drag portfolio_optimization_v1 onto the Canvas, wire ≥2 upstream price_factor_v1 nodes (or pass symbols directly), pick method (sharpe / min_variance / risk_parity). Output is the optimal weights dict + annualized return/vol/Sharpe + efficient_frontier.

Core formulas

μ = mean(returns) × 252          # annualized return vector
Σ = cov(returns)  × 252          # annualized covariance

Objectives:
  sharpe       : minimize -(μ·w - rf) / sqrt(w'Σw)
  min_variance : minimize w'Σw
  risk_parity  : minimize Σ_i (RC_i - σ_p/n)²

Constraints:
  sum(w) = 1, 0 ≤ w_i ≤ 1
  Starting w₀ = [1/n, ..., 1/n]

Solver: scipy.optimize.minimize(method='SLSQP', ftol=1e-9, maxiter=500)

Efficient frontier: 20 points, linspace(μ.min, μ.max);
  for each target_ret: minimize w'Σw s.t. sum=1, μ·w=target_ret

Assumptions & applicability

Assumptions: yfinance data available + Σ positive-definite + long-only + sum=1 + 1y lookback + 252 annualization + SLSQP.

Fits: cross-asset strategic allocation, efficient-frontier visualization, classic Markowitz scenarios.

Does not fit: leverage, shorting, Black-Litterman priors, robust / shrinkage, multi-period, transaction costs.

Input / Output contract

{symbols, period?, method?, risk_free_rate?}{symbols, method, period, weights: dict, annual_return, annual_volatility, sharpe_ratio, efficient_frontier: list[20], optimization_success, windows}.

Unknown method → silent fallback to sharpe. < 2 symbols → error.

Known limitations

  1. SLSQP corner solutions: some seeds converge the sharpe method to one asset — normal convex-optimization behaviour, not a bug
  2. risk_parity is non-convex → SLSQP local minimum (usually sufficient)
  3. ftol=1e-9 / maxiter=500 / 20 frontier points / bounds [0, 1] all hardcoded
  4. risk_free_rate used only by sharpe
  5. optimization_success=false silently falls back to equal weight — callers must check
  6. Per-asset risk contribution computable but not exposed

References

Markowitz (1952) original + Maillard et al. (2010) ERC + Kraft (1988) SLSQP + DeMiguel-Garlappi-Uppal (2009) 1/N baseline. Full citations in the frontmatter.

Golden Test

tests/golden/fixtures/tier2/portfolio_optimize_route/3-method parametrized over 4-symbol synthetic returns, 1e-10 tolerance post-round(4), 9 tests:

  • matches_reference[sharpe/min_variance/risk_parity] — 3 methods byte-equal
  • determinism — 5 reruns byte-identical
  • weights_sum_to_one — normalization invariant (all methods)
  • long_only_constraint — w ∈ [0, 1] (all methods)
  • efficient_frontier_structure — 20 points, each {return, volatility}, sorted ascending
  • rejects_single_symbol — <2 → error
  • invalid_method_falls_back_to_sharpe — unknown method ≡ sharpe

Note on testing history: the early golden (closed PR #39) wrongly targeted the legacy dead function analyze_portfolio_optimization — a user's sharp "Canvas looks fine" observation triggered the post-audit, which found Canvas actually POSTs /portfolio/optimize (this operator). The 2026-04-20 fix deleted the legacy function and rebuilt this golden.

Changelog

  • 1.0.0 (2026-04-20) — First Active (targets the real Canvas route after legacy cleanup)

Verifiable intelligence for the decisions that demand scrutiny.