# Allocation Engines ## Abstract The Portfolio Engine supports three distinct allocation engines, selectable via the `allocation_engine` configuration parameter. Each engine transforms expected returns and a covariance matrix into portfolio weights, but they differ fundamentally in their objectives, constraint handling, and mathematical formulations. This document describes each engine in detail. For the solver integration and constraint relaxation, see [RELAXATION_CASCADE.md](RELAXATION_CASCADE.md). --- ## 1. Engine Selection | ID | Name | Module | Objective | |----|------|--------|-----------| | 1 | Mean-Variance (CVXPY) | `cvxpy_engine.py` | Maximize risk-adjusted return subject to constraints | | 2 | Hierarchical Risk Parity (HRP) | `hrp_engine.py` | Allocate inversely to hierarchical cluster risk | | 3 | Exact True Risk Parity (ERC) | `erc_engine.py` | Equal marginal risk contribution across all assets | --- ## 2. Engine 1 — Mean-Variance Optimization (CVXPY) **`cvxpy_engine.py` → `CVXPYOptimizationEngine`** ### Objective Function The engine solves a convex quadratic program: ``` Maximize: μᵀw - (λ/2) · wᵀΣw - TC(w, w_prev) - Impact(w, w_prev) ``` where: - μ: Expected return vector - Σ: Covariance matrix (PSD-wrapped) - λ: Risk aversion coefficient (derived from `risk_input` 1–10) - TC: Transaction cost penalty (proportional to |Δw|) - Impact: Almgren-Chriss quadratic market impact ### Constraints | Constraint | Formula | Config Key | |------------|---------|------------| | Budget | Σwᵢ ≤ 1.0 | (always active) | | Asset floor | wᵢ ≥ single_asset_min | `single_asset_min` | | Asset cap | wᵢ ≤ single_asset_max | `single_asset_max` | | Sector cap | Σwᵢ ≤ sector_limit ∀ sector | `sector_limit` | | Gross leverage | ‖w‖₁ ≤ gross_leverage_cap | `gross_leverage_cap` | | Beta range | β_min ≤ βᵀw ≤ β_max | Derived from `risk_input` | | Duration cap | dᵀw ≤ D_max | Derived from `risk_input` (bonds only) | | Max turnover | Σ|Δwᵢ| ≤ max_turnover | `max_turnover` | | CVaR tail risk | CVaR_α(w) ≤ threshold | `cvar_enabled`, `cvar_alpha`, `cvar_lambda` | ### Cardinality Constraints **`cvxpy_engine.py` → Iterative Dropping Heuristic** Since standard open-source CVXPY solvers (CLARABEL, OSQP, ECOS, SCS) cannot handle mixed-integer constraints natively, cardinality is enforced via a post-optimization heuristic: 1. Solve the continuous relaxation (no integer constraints). 2. Count the number of assets with weight > ε (threshold: 0.001). 3. If the count exceeds `max_assets`, identify the smallest-weight positions. 4. Lock those positions to exactly 0.0 by adding equality constraints. 5. Re-solve with the reduced feasible set. 6. If the reduced problem is infeasible, roll back the dropped assets and accept the continuous solution. This greedy approach is not globally optimal but is sufficient for practical portfolios and avoids the NP-hard branch-and-bound required by true integer programming. ### Constraint Relaxation Cascade When the initial formulation is infeasible, the engine triggers a 7-stage cascade that progressively relaxes constraints until a feasible solution is found. See [RELAXATION_CASCADE.md](RELAXATION_CASCADE.md) for the full sequence. ### Tax-Aware Optimization When `tax_enabled = True`, the engine incorporates tax drag into the objective: - Short-term gains (holding period < 366 days) are penalised at `tax_rate_st` (default: 35%). - Long-term gains are penalised at `tax_rate_lt` (default: 20%). - The cost basis is tracked via a HIFO (Highest In, First Out) lot accounting system. --- ## 3. Engine 2 — Hierarchical Risk Parity (HRP) **`hrp_engine.py` → `hrp_allocation()`** ### Algorithm HRP is a graph-theoretic approach (López de Prado, 2016) that does not require expected returns or matrix inversion: 1. **Distance Matrix:** Compute pairwise distances from correlations: d(i,j) = √(0.5 · (1 - ρᵢⱼ)) 2. **Hierarchical Clustering:** Apply single-linkage agglomerative clustering to form a dendrogram. 3. **Quasi-Diagonalisation:** Reorder the covariance matrix rows/columns to follow the dendrogram leaf order. 4. **Recursive Bisection:** Split the sorted asset list into two clusters. Allocate risk inversely proportional to each cluster's variance. Recurse until each cluster is a single asset. ### Tax-Aware Blending **`hrp_engine.py` → `hrp_allocation_with_tax()`** When tax optimization is active, HRP weights are blended with the current holdings to minimise unnecessary realisation of gains: ``` w_final = (1 - λ_tax) · w_HRP + λ_tax · w_current ``` The blending parameter λ_tax is modulated by the gain fraction: positions with large unrealised gains receive more "inertia" to resist trading. --- ## 4. Engine 3 — Exact True Risk Parity (ERC) **`erc_engine.py` → `exact_risk_parity_allocation()`** ### Objective Exact Risk Parity ensures that every asset contributes equally to total portfolio risk. The marginal risk contribution of asset i is: ``` MRC_i = w_i · (Σw)_i ``` In a true ERC portfolio, MRC_i = MRC_j for all i, j. ### Spinu Logarithmic Barrier Formulation The engine solves this via the strictly convex formulation (Spinu, 2013): ``` Minimize: 0.5 · xᵀΣx - Σ ln(xᵢ) ``` The logarithmic barrier term -Σ ln(xᵢ) enforces strict positivity (xᵢ > 0) without requiring explicit constraints. The resulting optimal x* is normalised to portfolio weights: ``` w = x* / Σx*ᵢ ``` ### Solver The problem is solved using CVXPY with the SCS solver (fallback to ECOS). The logarithmic barrier makes the problem second-order cone representable. ### Important Properties - **All assets receive non-zero weight.** The ln(x) barrier forces every position to be strictly positive. This means cardinality constraints and single-asset maximums are intentionally bypassed. - **No expected returns required.** ERC depends only on the covariance matrix, making it purely risk-driven. - **Regime-Stressed Covariance.** Before solving, the covariance matrix is scaled by the HMM regime severity score via `regime_stress_covariance()`. ### When to Use ERC ERC is ideal when: - You have no reliable return forecasts and want a purely risk-diversified portfolio. - You want maximum diversification with mathematical guarantees on risk equality. - The macro regime is highly uncertain, making return estimates unreliable. --- ## 5. Multi-Period (MPC) Optimization **`solver.py` → `multi_period_optimize()`** The engine also supports a Model Predictive Control (MPC) multi-period optimizer that solves a stochastic program over a configurable horizon (default: 4 quarters): 1. **Scenario Fan:** Generates S correlated Monte Carlo scenarios via Cholesky decomposition of Σ. 2. **Non-Anticipativity:** Stage-1 decisions are constrained to be identical across all scenarios. 3. **Discounted Utility:** Maximises probability-weighted, time-discounted utility across the scenario tree. 4. **Tax Decay:** Applies geometric decay to tax rates over the planning horizon, modelling the long-term/short-term transition. If the MPC solver fails to converge, it falls back to the single-period Mean-Variance optimizer. --- ## References - Almgren, R., & Chriss, N. (2001). Optimal execution of portfolio transactions. *Journal of Risk*, 3(2), 5–39. - López de Prado, M. (2016). Building diversified portfolios that outperform out of sample. *Journal of Portfolio Management*, 42(4), 59–69. - Maillard, S., Roncalli, T., & Teïletche, J. (2010). The properties of equally weighted risk contribution portfolios. *Journal of Portfolio Management*, 36(4), 60–70. - Markowitz, H. (1952). Portfolio selection. *Journal of Finance*, 7(1), 77–91. - Spinu, F. (2013). An algorithm for computing risk parity weights. *SSRN Working Paper*.