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_retAssumptions & 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
- SLSQP corner solutions: some seeds converge the sharpe method to one asset — normal convex-optimization behaviour, not a bug
risk_parityis non-convex → SLSQP local minimum (usually sufficient)ftol=1e-9/maxiter=500/ 20 frontier points / bounds [0, 1] all hardcodedrisk_free_rateused only by sharpeoptimization_success=falsesilently falls back to equal weight — callers must check- 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-equaldeterminism— 5 reruns byte-identicalweights_sum_to_one— normalization invariant (all methods)long_only_constraint— w ∈ [0, 1] (all methods)efficient_frontier_structure— 20 points, each {return, volatility}, sorted ascendingrejects_single_symbol— <2 → errorinvalid_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)

