Factor Investing and Long/Short Equity › Multi-Factor Portfolio Construction

Michael BrenndoerferDecember 29, 20257 min read
Quantitative FinanceAll chapters
Part 11 of 15Quantitative Trading Strategies
Chapter 62Factor Investing and Long/Short Equity › Multi-Factor Portfolio Construction
Reading section 5 of 12 · Multi-Factor Portfolio Construction · 7 minSection 5 of 12 · 7 min

Book contents

Section 5 of 12 · 7 min read

Factor Investing and Long/Short Equity › Multi-Factor Portfolio ConstructionOne section · Sections in this chapter5 / 12 ↓
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

Most sophisticated quantitative strategies combine multiple factors rather than relying on a single characteristic. Multi-factor portfolios offer several advantages that make them superior to single-factor approaches for most investors:

  • Diversification: Factors have low correlations with each other, so combining them reduces portfolio volatility. When value underperforms, momentum might be doing well, and vice versa. This diversification benefit is similar to holding stocks in different industries.
  • Smoother returns: Different factors outperform in different market environments, creating more consistent performance. Value tends to do well during economic recoveries, while quality performs better during market stress. Combining factors smooths the return stream.
  • Higher Sharpe ratios: The risk reduction from factor diversification typically exceeds any reduction in expected returns. If each factor earns a premium, combining them captures multiple premia while benefiting from diversification. The result is higher risk-adjusted returns.

Combining Factor Scores

The most straightforward approach to multi-factor investing combines individual factor scores into a composite score. Each stock receives a score reflecting its standing across all factors, and portfolios are formed based on this composite. A stock might be moderately cheap, strongly momentum-positive, and highly profitable. The composite score aggregates these characteristics into a single ranking that can be used for portfolio construction.

In[13]:
Code
def compute_composite_score(df, factors, weights=None):
    """
    Compute composite factor score as weighted average of individual factors.

    Parameters:
    -----------
    df : DataFrame with factor z-scores
    factors : list of factor column names
    weights : list of factor weights (default: equal weights)
    """
    if weights is None:
        weights = [1.0 / len(factors)] * len(factors)

    composite = np.zeros(len(df))
    for factor, weight in zip(factors, weights):
        composite += weight * df[factor].values

    return composite


# Define factors and weights
factors = ["value_zscore", "momentum_zscore", "quality_zscore", "lowvol_zscore"]
factor_weights = [0.30, 0.25, 0.25, 0.20]  # Tilt toward value

stocks["composite_score"] = compute_composite_score(
    stocks, factors, factor_weights
)
stocks["composite_quintile"] = assign_quintiles(stocks["composite_score"])

# Analyze composite score characteristics
composite_stats = stocks.groupby("composite_quintile")[
    factors + ["composite_score"]
].mean()
Out[14]:
Console
Multi-Factor Quintile Portfolio Characteristics:
                    value_zscore  momentum_zscore  quality_zscore  \
composite_quintile                                                  
Q1                        -0.662           -0.740          -0.535   
Q2                        -0.232           -0.296          -0.281   
Q3                         0.071            0.000          -0.145   
Q4                         0.132            0.268           0.379   
Q5                         0.691            0.768           0.581   

                    lowvol_zscore  composite_score  
composite_quintile                                  
Q1                         -0.620           -0.641  
Q2                         -0.146           -0.243  
Q3                          0.091            0.003  
Q4                          0.240            0.249  
Q5                          0.434            0.631

The composite quintile portfolios show the expected pattern: Q5 (the long portfolio) has positive scores across all factors, while Q1 (the short portfolio) has negative scores. The composite approach captures multiple sources of alpha simultaneously. Stocks in the top quintile are cheap, have strong momentum, high profitability, and low volatility. These stocks benefit from multiple tailwinds, increasing the probability that at least some factor premia will be realized.

Factor Weight Optimization

Rather than using equal or heuristic weights, factor weights can be optimized to maximize the Sharpe ratio of the combined portfolio. This requires historical factor return estimates and covariance matrices, as we covered in Part IV on portfolio optimization. The goal is to find the combination of factor weights that maximizes risk-adjusted returns given the expected returns and correlations of each factor.

The optimization problem seeks to maximize the portfolio's Sharpe ratio, assuming a risk-free rate of zero for long/short portfolios since these strategies are self-financing:

max⁡ww⊤μw⊤Σw\max_w \frac{w^\top \mu}{\sqrt{w^\top \Sigma w}}

where:

  • ww: vector of factor weights representing the allocation to each factor
  • μ\mu: vector of expected factor excess returns capturing what we expect each factor to earn
  • Σ\Sigma: factor return covariance matrix describing both factor volatilities and their correlations
  • w⊤μw^\top \mu: expected portfolio return, the weighted average of factor returns
  • w⊤Σw\sqrt{w^\top \Sigma w}: portfolio volatility, accounting for diversification benefits from low factor correlations

To derive the optimal weights, we observe that the Sharpe ratio is scale-invariant, meaning that doubling all weights does not change the ratio. We can therefore fix the expected return to a constant level (e.g., w⊤μ=1w^\top \mu = 1) and minimize the portfolio variance. This transformation converts the ratio optimization into a standard quadratic programming problem. The Lagrangian for this constrained optimization is:

L(w,λ)=12w⊤Σw−λ(w⊤μ−1)\mathcal{L}(w, \lambda) = \frac{1}{2}w^\top \Sigma w - \lambda(w^\top \mu - 1)

where:

  • L\mathcal{L}: Lagrangian function to be minimized, combining the objective with the constraint
  • ww: vector of factor weights we are solving for
  • Σ\Sigma: factor return covariance matrix encoding risk relationships
  • λ\lambda: Lagrange multiplier enforcing the return constraint, representing the shadow price of the return target
  • μ\mu: vector of expected factor excess returns

The first term represents the portfolio variance (to be minimized), while the second term captures the constraint that the expected return must equal 1. This formulation follows the standard approach in constrained optimization where we convert an inequality or equality constraint into part of the objective function.

Taking the derivative with respect to ww and setting it to zero yields the first-order optimality condition:

∂L∂w=Σw−λμ=0(differentiate and set to zero)Σw=λμ(rearrange terms)w=λΣ−1μ(solve for w)\begin{aligned} \frac{\partial \mathcal{L}}{\partial w} = \Sigma w - \lambda \mu &= 0 && \text{(differentiate and set to zero)} \\ \Sigma w &= \lambda \mu && \text{(rearrange terms)} \\ w &= \lambda \Sigma^{-1} \mu && \text{(solve for } w \text{)} \end{aligned}

Since λ\lambda is just a scalar scaling factor that does not affect relative weights, the relative weights are determined solely by the product of the inverse covariance matrix and the expected returns:

w∗∝Σ−1μw^* \propto \Sigma^{-1} \mu

where:

  • w∗w^*: optimal factor weights that maximize the Sharpe ratio
  • Σ−1\Sigma^{-1}: inverse of the covariance matrix (also called the precision matrix), encoding information about factor volatilities and correlations
  • μ\mu: expected factor excess returns

This elegant formula has a clear intuition. The precision matrix Σ−1\Sigma^{-1} automatically accounts for correlations between factors. It assigns higher weights to factors with high returns and low volatility, while penalizing factors that are highly correlated with others to maximize diversification benefits. A factor with high expected returns but also high correlation with other factors receives less weight than a factor with moderate returns but unique diversification properties.

In[15]:
Code
# Simulate historical factor returns (monthly data, 120 months)
np.random.seed(123)
n_months = 120

# Factor expected returns (annualized): Value 4%, Momentum 6%, Quality 3%, Low Vol 2%
annual_returns = np.array([0.04, 0.06, 0.03, 0.02])
monthly_returns = annual_returns / 12

# Factor volatilities (annualized): 8%, 12%, 6%, 5%
annual_vols = np.array([0.08, 0.12, 0.06, 0.05])
monthly_vols = annual_vols / np.sqrt(12)

# Factor correlation matrix
factor_corr = np.array(
    [
        [1.0, -0.4, 0.2, 0.3],
        [-0.4, 1.0, 0.1, -0.2],
        [0.2, 0.1, 1.0, 0.25],
        [0.3, -0.2, 0.25, 1.0],
    ]
)

# Convert to covariance matrix
factor_cov = np.outer(monthly_vols, monthly_vols) * factor_corr

# Generate simulated factor returns
factor_returns = np.random.multivariate_normal(
    monthly_returns, factor_cov, n_months
)
factor_returns_df = pd.DataFrame(
    factor_returns, columns=["Value", "Momentum", "Quality", "LowVol"]
)
In[16]:
Code
from scipy.optimize import minimize


def max_sharpe_weights(expected_returns, cov_matrix):
    """
    Find factor weights that maximize the Sharpe ratio.

    Uses constrained optimization with weights summing to 1 and non-negative.
    """
    n_factors = len(expected_returns)

    def neg_sharpe(weights):
        port_return = np.dot(weights, expected_returns)
        port_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
        return -port_return / port_vol if port_vol > 0 else 0

    constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
    bounds = [(0, 1) for _ in range(n_factors)]
    initial = np.ones(n_factors) / n_factors

    result = minimize(
        neg_sharpe,
        initial,
        method="SLSQP",
        bounds=bounds,
        constraints=constraints,
    )
    return result.x


# Calculate sample statistics and optimize
sample_returns = factor_returns_df.mean().values
sample_cov = factor_returns_df.cov().values

optimal_weights = max_sharpe_weights(sample_returns, sample_cov)

# Compare equal vs optimal weighting
equal_weights = np.ones(4) / 4
equal_return = np.dot(equal_weights, sample_returns) * 12
equal_vol = np.sqrt(
    np.dot(equal_weights.T, np.dot(sample_cov * 12, equal_weights))
)
equal_sharpe = equal_return / equal_vol

opt_return = np.dot(optimal_weights, sample_returns) * 12
opt_vol = np.sqrt(
    np.dot(optimal_weights.T, np.dot(sample_cov * 12, optimal_weights))
)
opt_sharpe = opt_return / opt_vol
Out[17]:
Console
Optimal Factor Weights (Max Sharpe):
  Value: 31.2%
  Momentum: 28.7%
  Quality: 39.8%
  LowVol: 0.3%

Equal-Weighted Portfolio: Return=4.7%, Vol=3.7%, Sharpe=1.27
Optimized Portfolio: Return=5.7%, Vol=4.2%, Sharpe=1.35

The optimization tilts heavily toward factors with favorable risk-return characteristics. In this example, Quality receives a large weight due to its high Sharpe ratio (modest returns but low volatility), while Momentum, despite having the highest expected return, receives less weight due to its higher volatility and negative correlation with Value. The negative correlation between Value and Momentum provides diversification, which the optimization accounts for through the covariance matrix inversion. The result is a portfolio that achieves a higher Sharpe ratio than simple equal weighting.