A Complete Tutorial on Market Implied Returns from Factor Models
In portfolio optimization, we need three key inputs:
While covariance matrices can be estimated reliably from historical data (they're relatively stable), expected returns are notoriously difficult to estimate. Small errors in expected returns lead to large errors in optimal portfolios.
Historical average returns are poor predictors of future returns. A 10-year sample gives us maybe 10 independent annual observations - far too few to estimate expected returns with any precision.
Standard error of mean return estimate: \(\frac{\sigma}{\sqrt{T}} \approx \frac{16\%}{\sqrt{10}} = 5\%\)
This means our confidence interval for equity expected returns might be [-2%, +12%] - essentially useless for portfolio construction!
Julian Lorenz developed two practical methods that leverage the BRISMA factor model to derive expected returns. The key insight is:
If asset returns are driven by common factors, then expected returns should also be driven by those factors.
\[\mu_i = \beta_{i,1} \cdot \lambda_1 + \beta_{i,2} \cdot \lambda_2 + \ldots\]The two methods differ in how they estimate the factor premiums \(\lambda_k\):
Approach: Use the current 10-year German Bund yield to back out the factor premium.
Philosophy: Market prices contain forward-looking information.
Type: Forward-looking
Approach: Use time-weighted historical factor returns.
Philosophy: Past performance contains information about future returns (momentum).
Type: Backward-looking
A factor model decomposes asset returns into systematic and idiosyncratic components:
Where:
Taking expectations on both sides:
\[\E[r_i] = \alpha_i + \beta_{i,1} \cdot \E[f_1] + \E[\epsilon_i]\]Since \(\E[\epsilon_i] = 0\):
\[\mu_i = \alpha_i + \beta_{i,1} \cdot \mu_{f_1}\]In equilibrium, there should be no "free lunch." If an asset has no systematic risk (\(\beta = 0\)), it shouldn't earn more than the risk-free rate. This implies:
\[\alpha_i = 0 \quad \text{for all assets}\]Therefore:
\[\boxed{\mu_i = \beta_{i,1} \cdot \lambda}\]where \(\lambda = \E[f_1]\) is the factor risk premium.
The formula \(\mu_i = \beta_{i,1} \cdot \lambda\) has a beautiful interpretation:
| Term | Meaning | Units |
|---|---|---|
| \(\beta_{i,1}\) | Quantity of Factor 1 risk in asset \(i\) | Dimensionless |
| \(\lambda\) | Price per unit of Factor 1 risk | % per year |
| \(\mu_i\) | Total compensation for bearing Factor 1 risk | % per year |
If the factor premium is \(\lambda = 2\%\) per year:
We have the betas \(\beta_{i,1}\) from the factor model estimation (PCA on the covariance matrix). What we don't have is the factor premium \(\lambda\).
Julian's key insight: We can back out \(\lambda\) from a reference asset where we have a reasonable estimate of expected return!
For a buy-and-hold investor, the yield-to-maturity of a bond approximates its expected return.
Specifically, for the 10-year German Bund:
\[\E[r_{10Y}] \approx y_{10Y} - r_f\]where \(y_{10Y}\) is the 10-year yield and \(r_f\) is the overnight rate (ESTR).
This assumption is reasonable because:
Compute the expected excess return of the 10-year bond:
\[r_{target} = \ln\left(\frac{1 + y_{10Y}}{1 + r_f}\right)\]Example: With \(y_{10Y} = 2.8\%\) and \(r_f = 2.4\%\):
\[r_{target} = \ln\left(\frac{1.028}{1.024}\right) = +0.39\%\]Using the factor model equation for the reference bond:
\[\mu_{bond} = \beta_{bond,1} \cdot \lambda\]Solving for \(\lambda\):
\[\lambda = \frac{r_{target}}{\beta_{bond,1}}\]Example: With \(\beta_{bond,1} = 0.95\):
\[\lambda = \frac{0.39\%}{0.95} = 0.41\%\]Apply the factor model to every asset in the portfolio:
\[\mu_i = \beta_{i,1} \cdot \lambda\]Example:
# Step 1: Define target return from yield curve
yield_10y <- 0.028 # 10-year German Bund yield
estr_rate <- 0.024 # ESTR overnight rate
return_asset_10year <- log((1 + yield_10y) / (1 + estr_rate)) # ~0.39%
# Step 2: Back out lambda using reference bond's beta
# Julian's comment: "The factor-risk-premium lambda_ is not estimated
# but rather calibrated based on the 10-year-rate"
lambda_ <- return_asset_10year / beta_id_id_comp.fit["comp1", reference_bond_id]
# Step 3: Project to all assets
# Julian's comment: "For all the assets of the model the expected
# excess returns are computed via the given betas"
mu_implied <- as.vector(t(beta_id_id_comp.fit["comp1", ]) * lambda_)
Past factor performance contains information about future factor returns.
This is related to the momentum effect: assets that performed well recently tend to continue performing well.
The factor returns are the principal component returns from the BRISMA model. These are already computed during covariance estimation.
Recent returns get higher weight (exponential decay):
\[w_t = \frac{(1-\delta)^{T-t}}{\sum_{s=1}^{T}(1-\delta)^{T-s}}\]where \(\delta\) is the decay rate (same as used for covariance estimation).
Then annualize appropriately.
Same formula as Method 1:
\[\mu_i = \beta_{i,1} \cdot \hat{\mu}_{f_1}\]# Step 1-2: Compute weighted factor returns
# Julian uses the same weights as covariance estimation
# Rolling returns of principal components
ret_roll_rm_comp <- ret22_date_id_rm %*% ei_port$vectors
# Time and GARCH weights (already computed in covariance estimation)
weighted_returns <- weight_time_garch * ret_roll_rm_comp
# Step 3: Sum weighted returns (annualized)
# Julian's comment: "The comp1_return_weighted represents the weighted
# historical returns of comp1"
comp1_return_weighted <- sum(weighted_returns[, 1]) * annualization_factor
# Step 4: Project to assets via betas
mu_from_past <- as.vector(t(beta_id_id_comp.fit["comp1", ]) * comp1_return_weighted)
The time-weighted average has several advantages:
| Advantage | Explanation |
|---|---|
| Regime sensitivity | Adapts to changing market conditions |
| Consistency | Same weights as covariance estimation (coherent model) |
| Momentum capture | Recent performance weighted more heavily |
| GARCH integration | High-volatility periods get appropriate weight |
Both methods project expected returns using:
\[\mu_i = \beta_{i,1} \cdot \lambda\]But what if Factor 1 explains very little of asset \(i\)'s variance? Then the projected expected return is essentially meaningless!
R-squared measures what fraction of an asset's variance is explained by Factor 1:
\[R^2_i = \frac{\beta_{i,1}^2 \cdot \sigma^2_{f_1}}{\sigma^2_i} = \frac{\text{Systematic Variance}}{\text{Total Variance}}\]From the factor model:
\[r_i = \beta_{i,1} \cdot f_1 + \epsilon_i\]Taking variance:
\[\Var(r_i) = \beta_{i,1}^2 \cdot \Var(f_1) + \Var(\epsilon_i)\] \[\sigma^2_i = \beta_{i,1}^2 \cdot \sigma^2_{f_1} + \sigma^2_{\epsilon,i}\]R-squared is the fraction explained by the factor:
\[R^2_i = \frac{\beta_{i,1}^2 \cdot \sigma^2_{f_1}}{\sigma^2_i}\]| R-squared | Interpretation | Recommendation |
|---|---|---|
| > 80% | Factor dominates asset risk | High confidence in \(\mu_i = \beta_i \lambda\) |
| 50% - 80% | Mixed systematic/idiosyncratic | Use with caution; consider blending |
| < 50% | Idiosyncratic risk dominates | Factor-based return unreliable; use alternatives |
Given:
Bond R-squared:
\[R^2_{bond} = \frac{0.95^2 \times 0.04^2}{0.04^2} = 0.95^2 = 0.90 \quad (90\%)\]Equity R-squared:
\[R^2_{equity} = \frac{0.25^2 \times 0.04^2}{0.16^2} = \frac{0.0001}{0.0256} = 0.004 \quad (0.4\%)\]Conclusion: Factor-based expected returns are reliable for bonds, but essentially meaningless for equities!
# Julian's comment: "The explained variance R^2 per asset comes with the idea
# that the market implied return coming from our model should only be
# considered to be reliable for those assets where R^2 is high"
# Calculate R-squared for each asset
var_factor1 <- var(factor_returns[, 1])
var_assets <- diag(Q_emp) # Empirical variances
R_squared <- (beta_comp1^2 * var_factor1) / var_assets
# Create reliability table
tbl_mir <- tibble(
asset = asset_names,
mu_implied = mu_implied,
R_squared = R_squared,
reliable = R_squared > 0.50
)
| Aspect | Method 1: 10Y Bond | Method 2: Historical |
|---|---|---|
| Data Source | Current yield curve | Historical returns |
| Philosophy | Market expectations matter | Momentum persists |
| Time Orientation | Forward-looking | Backward-looking |
| Anchor | Observable market rate | Historical average |
| Best for | Fixed-income portfolios | Trend-following strategies |
| Weakness | Relies on yield = expected return | Past may not predict future |
A natural extension is to blend the two methods based on R-squared:
This automatically:
| Parameter | Bond (Euro Gov) | Equity (MSCI EMU) |
|---|---|---|
| Volatility (\(\sigma\)) | 4% | 16% |
| Beta on Factor 1 | 0.95 | 0.25 |
Market Rates:
Historical Data:
| Asset | Method 1 | Method 2 | Difference | R-squared | Reliability |
|---|---|---|---|---|---|
| Bond | +0.39% | +3.33% | 2.94% | 90% | High |
| Equity | +0.10% | +0.88% | 0.78% | 0.4% | Unreliable |
The methods give very different answers for bonds (3% difference!). Method 1 is forward-looking (current yield curve), Method 2 is backward-looking (historical momentum). Neither is "correct" - the choice depends on your investment philosophy.
For equities, both methods are unreliable because R-squared is only 0.4%. Consider using fundamental analysis or other approaches.
Before running Julian's methods, you need outputs from the BRISMA factor model:
# Run the BRISMA pipeline first
source("R/examples/example1.R")
# You now have:
# - beta_id_id_comp.fit: Factor betas for each asset
# - ei_port: Eigenvalue decomposition
# - ret22_date_id_rm: Rolling returns of risk model assets
# - weight_time_garch: Time and GARCH weights
# ===========================================================
# METHOD 1: 10Y Bond Calibration
# ===========================================================
# Market data (update these with current values)
yield_10y <- 0.028 # 10-year German Bund yield
estr_rate <- 0.024 # ESTR overnight rate
# Reference asset (10Y bond proxy)
asset_10year <- "RX_10Y_EUR" # Or your bond index name
reference_id <- tbl_map_id_name %>%
filter(name == asset_10year) %>%
pull(id)
# Step 1: Calculate target excess return
return_asset_10year <- log((1 + yield_10y) / (1 + estr_rate))
cat("Target bond excess return:", round(return_asset_10year * 100, 2), "%\n")
# Step 2: Back out lambda
beta_ref <- beta_id_id_comp.fit["comp1", paste0(reference_id, ".fit")]
lambda_ <- return_asset_10year / beta_ref
cat("Factor premium (lambda):", round(lambda_ * 100, 2), "%\n")
# Step 3: Project to all assets
mu_method1 <- as.vector(t(beta_id_id_comp.fit["comp1", ]) * lambda_)
names(mu_method1) <- gsub("\\.fit$", "", colnames(beta_id_id_comp.fit))
# Step 4: Calculate R-squared
var_comp1 <- var(ret_roll_rm_comp[, 1]) # Factor 1 variance
var_assets <- diag(Q_port_emp) # Asset variances
beta_vec <- as.vector(beta_id_id_comp.fit["comp1", ])
R_squared <- (beta_vec^2 * var_comp1) / var_assets[names(beta_vec)]
# Create results table
tbl_mir_method1 <- tibble(
id = names(mu_method1),
name = tbl_map_id_name$name[match(names(mu_method1), tbl_map_id_name$id)],
mu_implied = mu_method1,
R_squared = R_squared,
reliable = R_squared > 0.50
)
print(tbl_mir_method1)
# ===========================================================
# METHOD 2: Historical Performance
# ===========================================================
# Step 1: Get principal component returns
ret_roll_rm_comp <- ret22_date_id_rm %*% ei_port$vectors
# Step 2: Apply time and GARCH weights
# (These are already computed in the BRISMA pipeline)
weighted_returns <- weight_time_garch * ret_roll_rm_comp
# Step 3: Compute weighted average factor return
# Annualization factor
annualization <- (365.2425 / 7 * 5) / 22 # ~11.86 periods per year
comp1_return_weighted <- sum(weighted_returns[, 1]) * annualization
cat("Historical factor return:", round(comp1_return_weighted * 100, 2), "%\n")
# Step 4: Project to all assets
mu_method2 <- as.vector(t(beta_id_id_comp.fit["comp1", ]) * comp1_return_weighted)
names(mu_method2) <- gsub("\\.fit$", "", colnames(beta_id_id_comp.fit))
# Create results table
tbl_mir_method2 <- tibble(
id = names(mu_method2),
name = tbl_map_id_name$name[match(names(mu_method2), tbl_map_id_name$id)],
mu_implied = mu_method2
)
print(tbl_mir_method2)
# ===========================================================
# COMBINED RESULTS TABLE
# ===========================================================
tbl_mir_combined <- tibble(
id = names(mu_method1),
name = tbl_map_id_name$name[match(names(mu_method1), tbl_map_id_name$id)],
mu_method1 = mu_method1,
mu_method2 = mu_method2,
difference = abs(mu_method1 - mu_method2),
R_squared = R_squared,
reliable = R_squared > 0.50,
recommended = ifelse(R_squared > 0.50, mu_method1, mu_method2)
) %>%
arrange(desc(R_squared))
# Print nicely formatted
print(tbl_mir_combined, n = 20)
# Summary statistics
cat("\n=== Summary ===\n")
cat("Assets with reliable factor model (R² > 50%):", sum(tbl_mir_combined$reliable), "\n")
cat("Assets with unreliable factor model:", sum(!tbl_mir_combined$reliable), "\n")
cat("Average difference between methods:", round(mean(tbl_mir_combined$difference) * 100, 2), "%\n")
Suppose the yield curve inverts: 10Y yield = 3.0%, overnight rate = 3.5%.
1. Target return: \(\ln(1.030/1.035) = -0.48\%\)
2. Lambda is negative: \(\lambda = -0.48\%/0.95 = -0.51\%\)
3. All assets with positive beta have negative expected returns. This reflects the market's view that bonds will underperform cash.
A CTA fund has \(\beta = 0.05\) on Factor 1 and volatility of 10%.
1. R-squared: \(\frac{0.05^2 \times 0.04^2}{0.10^2} = \frac{0.000004}{0.01} = 0.0004 = 0.04\%\)
2. No - R-squared is essentially zero. Factor 1 explains almost none of the CTA's risk.
3. Use Method 2 (historical performance), fundamental analysis, or treat the CTA as a separate asset class with its own expected return estimate.
Suppose you have two factors with premiums \(\lambda_1 = 0.5\%\) and \(\lambda_2 = 2.0\%\).
An asset has betas: \(\beta_1 = 0.8\), \(\beta_2 = 0.3\).
1. Expected return: \(\mu = 0.8 \times 0.5\% + 0.3 \times 2.0\% = 0.4\% + 0.6\% = 1.0\%\)
2. Factor 1 contributes 0.4%, Factor 2 contributes 0.6%. Factor 2 is more important despite lower beta because it has a higher premium.
Given:
Calculate the hybrid expected return using the blending formula.
Hybrid formula: \(\mu^{hybrid} = R^2 \times \mu^{M1} + (1-R^2) \times \mu^{M2}\)
\(\mu^{hybrid} = 0.60 \times 1.0\% + 0.40 \times 4.0\% = 0.6\% + 1.6\% = 2.2\%\)
The hybrid gives 60% weight to Method 1 and 40% weight to Method 2.
Factor Pricing Theory:
Bond Yield and Expected Returns:
Momentum and Historical Returns:
For a rigorous academic treatment of factor risk premia with real Fama-French data (T=726 months, 1963-2023), see:
Key topics covered in the academic primer: