Advanced Portfolio Construction Techniques › Practical Constraints in Portfolio Construction
Quantitative FinanceAll chapters Advanced Portfolio Construction Techniques › Practical Constraints in Portfolio ConstructionOne section · Sections in this chapter5 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Real-world portfolios face numerous constraints beyond the basic budget constraint that weights must sum to one. Incorporating these constraints turns portfolio optimization from a theoretical exercise into a practical tool that produces implementable recommendations. A portfolio that is "optimal" in theory but violates regulatory limits or exceeds trading capacity is useless in practice.
Common Constraint Types
You typically face several categories of constraints, each arising from different practical considerations:
- Position limits: Minimum and maximum weights for individual assets. Regulatory requirements, risk limits, or liquidity concerns often mandate these bounds. For example, a mutual fund might be prohibited from holding more than 10% in any single security, or a pension fund might require minimum allocations to certain asset classes.
- Sector/industry constraints: Maximum exposure to any single sector or minimum allocation to certain sectors (e.g., ESG mandates). These constraints ensure diversification across different parts of the economy and can implement policy objectives like promoting sustainable investment.
- Turnover constraints: Limits on how much the portfolio can change from one period to the next, driven by transaction costs or tax considerations. Frequent rebalancing incurs trading costs and may trigger taxable events, so many investors limit how aggressively they adjust positions.
- Factor exposure constraints: Limits on exposure to risk factors like beta, size, or value. These constraints allow you to control your portfolio's sensitivity to systematic risk drivers, maintaining a desired risk profile regardless of individual security selection.
- Cardinality constraints: Limits on the number of assets in the portfolio (often driven by monitoring costs). Managing a portfolio of 500 securities requires more resources than managing 50, so some investors cap the number of holdings.
Implementing Constraints
Let us build a constrained portfolio optimizer that handles position limits, sector constraints, and turnover simultaneously. The key insight is that most constraints can be expressed as linear or simple nonlinear functions of the weights, allowing standard optimization algorithms to find solutions efficiently.
def constrained_portfolio(
mu,
cov,
risk_aversion=2.5,
min_weight=0.0,
max_weight=0.4,
sector_map=None,
max_sector=0.5,
prev_weights=None,
max_turnover=0.3,
):
"""
Optimize portfolio with practical constraints.
Parameters:
-----------
mu : array
Expected returns
cov : array
Covariance matrix
risk_aversion : float
Risk aversion parameter
min_weight, max_weight : float
Individual position bounds
sector_map : dict
Maps sector name to list of asset indices
max_sector : float
Maximum allocation to any sector
prev_weights : array
Previous portfolio weights (for turnover constraint)
max_turnover : float
Maximum one-way turnover
"""
n = len(mu)
def objective(w):
ret = w @ mu
risk = w @ cov @ w
return -(ret - 0.5 * risk_aversion * risk)
constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}]
# Sector constraints
if sector_map is not None:
for sector, indices in sector_map.items():
constraints.append(
{
"type": "ineq",
"fun": lambda w, idx=indices: (
max_sector - sum(w[i] for i in idx)
),
}
)
# Turnover constraint (one-way)
if prev_weights is not None:
constraints.append(
{
"type": "ineq",
"fun": lambda w: (
max_turnover - 0.5 * np.sum(np.abs(w - prev_weights))
),
}
)
# Position bounds
bounds = [(min_weight, max_weight) for _ in range(n)]
result = minimize(
objective,
np.ones(n) / n,
method="SLSQP",
constraints=constraints,
bounds=bounds,
options={"maxiter": 1000},
)
return result.x, result.success
# Define sectors
sector_map = {
"Tech": [0, 2], # Assets A and C
"Finance": [1, 3], # Assets B and D
"Consumer": [4], # Asset E
}
# Previous portfolio (for turnover constraint)
prev_weights = np.array([0.20, 0.20, 0.20, 0.20, 0.20])
# Unconstrained
w_unconstrained = optimize_portfolio(true_mu, true_cov)
# Define constraints
min_w, max_w = 0.05, 0.35
max_sect_exp = 0.45
max_turn = 0.15
# With constraints
w_constrained, success = constrained_portfolio(
true_mu,
true_cov,
min_weight=min_w,
max_weight=max_w,
sector_map=sector_map,
max_sector=max_sect_exp,
prev_weights=prev_weights,
max_turnover=max_turn,
)Constrained Portfolio Optimization ============================================================ Constraints applied: • Position bounds: 5% - 35% • Max sector exposure: 45% • Max turnover: 15% (one-way) Asset Unconstrained Constrained Δ from Prev ------------------------------------------------------------ Asset A 17.80% 17.80% -2.20% Asset B 14.77% 14.77% -5.23% Asset C 23.65% 23.65% +3.65% Asset D 23.06% 23.06% +3.06% Asset E 20.72% 20.72% +0.72% Constraint Verification: ---------------------------------------- Total turnover: 7.44% (limit: 15%) Tech exposure: 41.45% (limit: 45%) Finance exposure: 37.83% (limit: 45%) Consumer exposure: 20.72% (limit: 45%)
The constrained optimization respects all limits: no asset exceeds 35% weight, sector exposures are capped, and turnover is limited to 15%. While these constraints reduce the theoretical utility compared to an unconstrained optimum, they produce an implementable portfolio. The gap between the constrained and unconstrained solutions represents the "cost" of being practical, but this cost is usually worth paying for a portfolio that can be traded.
Transaction Cost Optimization
When rebalancing a portfolio, transaction costs can significantly impact returns. Every trade incurs costs from bid-ask spreads, market impact, and commissions. For large institutional portfolios, market impact alone can represent tens of basis points per trade. We can incorporate estimated costs directly into the optimization, finding portfolios that balance the benefits of rebalancing against the costs of trading.
def portfolio_with_transaction_costs(
mu, cov, prev_weights, transaction_cost=0.001, risk_aversion=2.5
):
"""
Optimize portfolio considering transaction costs.
Transaction costs are modeled as a linear penalty on turnover.
"""
n = len(mu)
def objective(w):
# Expected utility
ret = w @ mu
risk = w @ cov @ w
utility = ret - 0.5 * risk_aversion * risk
# Transaction cost penalty
turnover = np.sum(np.abs(w - prev_weights))
cost = transaction_cost * turnover
return -(utility - cost)
constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}]
bounds = [(0, 1) for _ in range(n)]
result = minimize(
objective,
prev_weights,
method="SLSQP",
constraints=constraints,
bounds=bounds,
)
return result.x
# Compare different transaction cost assumptions
tc_levels = [0, 0.0005, 0.001, 0.005]
tc_portfolios = {}
for tc in tc_levels:
w = portfolio_with_transaction_costs(true_mu, true_cov, prev_weights, tc)
tc_portfolios[tc] = wImpact of Transaction Costs on Optimal Rebalancing ====================================================================== Asset TC=0bp TC=5bp TC=10bp TC=50bp ---------------------------------------------------------------------- Asset A 17.80% 18.70% 19.51% 20.00% Asset B 14.77% 16.41% 17.27% 20.00% Asset C 23.65% 23.31% 22.67% 20.00% Asset D 23.06% 21.13% 20.44% 20.00% Asset E 20.72% 20.44% 20.11% 20.00% ---------------------------------------------------------------------- bp = basis points (1bp = 0.01%)
As transaction costs increase, the optimal rebalancing becomes less aggressive. With zero transaction costs, the optimizer freely adjusts weights to chase the best risk-adjusted returns. But at higher cost levels, the optimizer avoids small adjustments where the marginal benefit in risk-adjusted return does not justify the trading expense. This leads to weights closer to the previous portfolio, a phenomenon sometimes called "no-trade regions." The width of these regions depends on the transaction cost level: higher costs mean wider regions and less frequent rebalancing.
