Factor Investing and Long/Short Equity › Multi-Factor Portfolio Construction
Quantitative FinanceAll chapters 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.
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()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.631The 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:
where:
- : vector of factor weights representing the allocation to each factor
- : vector of expected factor excess returns capturing what we expect each factor to earn
- : factor return covariance matrix describing both factor volatilities and their correlations
- : expected portfolio return, the weighted average of factor returns
- : 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., ) and minimize the portfolio variance. This transformation converts the ratio optimization into a standard quadratic programming problem. The Lagrangian for this constrained optimization is:
where:
- : Lagrangian function to be minimized, combining the objective with the constraint
- : vector of factor weights we are solving for
- : factor return covariance matrix encoding risk relationships
- : Lagrange multiplier enforcing the return constraint, representing the shadow price of the return target
- : 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 and setting it to zero yields the first-order optimality condition:
Since 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:
where:
- : optimal factor weights that maximize the Sharpe ratio
- : inverse of the covariance matrix (also called the precision matrix), encoding information about factor volatilities and correlations
- : expected factor excess returns
This elegant formula has a clear intuition. The precision matrix 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.
# 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"]
)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_volOptimal 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.