Advanced Portfolio Construction Techniques › Practical Constraints in Portfolio Construction

Michael BrenndoerferDecember 18, 20257 min read
Quantitative FinanceAll chapters
Part 9 of 15Portfolio Theory and Investment Analysis
Chapter 51Advanced Portfolio Construction Techniques › Practical Constraints in Portfolio Construction
Reading section 5 of 11 · Practical Constraints in Portfolio Construction · 7 minSection 5 of 11 · 7 min

Book contents

Section 5 of 11 · 7 min read

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.

In[20]:
Code
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,
)
Out[21]:
Console
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.

In[22]:
Code
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] = w
Out[23]:
Console
Impact 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.

Out[24]:
Visualization
Bar chart showing resulting portfolio turnover (y-axis) for four transaction cost levels from 0 to 50 basis points (x-axis). Bars decrease steeply as costs rise, showing that higher transaction costs strongly suppress optimal rebalancing activity.
Impact of transaction costs on optimal portfolio turnover and rebalancing behavior. Higher costs lead to less aggressive rebalancing as the optimizer maintains positions closer to starting weights to avoid the erosion of returns by trading expenses.