Skip to main content

Risk-Premium PCA for estimating latent asset-pricing factors

Project description

RPPCA — Risk-Premium PCA for Asset Pricing

Estimate latent asset-pricing factors that explain both covariance and expected returns.

InstallationQuick StartAlgorithmAPI ReferenceLook-Ahead BiasCitation


Overview

RPPCA is a Python implementation of the Risk-Premium PCA (RP-PCA) method from:

Lettau, M. & Pelger, M. (2020). "Estimating latent asset-pricing factors." Journal of Econometrics, 218(1), 1–31.

Standard PCA finds factors that maximize explained variance, but ignores expected returns. RP-PCA generalizes PCA by adding a penalty term for pricing errors, enabling it to:

  • 🔍 Detect weak factors with high Sharpe ratios that PCA misses entirely
  • 📈 Produce factors with higher Sharpe ratios — often 2× those of PCA
  • 📉 Achieve smaller out-of-sample pricing errors
  • 📊 Explain the same amount of variance as conventional PCA

Installation

pip install rppca

Requirements: Python ≥ 3.9, NumPy ≥ 1.21

Quick Start

In-Sample Estimation

import numpy as np
from rppca import RPPCA

# X: excess returns matrix (T periods × N assets)
X = np.random.randn(600, 200)

# Estimate 5 factors with γ=10 (paper's recommended value)
model = RPPCA(n_factors=5, gamma=10)
model.fit(X)

# Access results
print(model.factors_.shape)       # (600, 5)
print(model.loadings_.shape)      # (200, 5)
print(model.eigenvalues_)         # Top 5 eigenvalues

# Evaluation metrics
print(f"Max Sharpe Ratio: {model.get_max_sharpe_ratio():.4f}")
print(f"RMS Pricing Error: {model.get_rms_pricing_error(X):.6f}")
print(f"Explained Variation: {model.get_explained_variation(X):.2%}")

Out-of-Sample Estimation (No Look-Ahead Bias)

from rppca import RollingRPPCA

# Rolling window: estimate loadings on past 240 months, project forward
rolling = RollingRPPCA(n_factors=5, gamma=10, window=240)
rolling.fit(X)

# Out-of-sample factors (strictly causal — no future information used)
print(rolling.oos_factors_.shape)  # (360, 5) — T minus window
print(rolling.oos_start_)          # 240

# Out-of-sample evaluation
print(f"OOS Max Sharpe Ratio: {rolling.get_oos_max_sharpe_ratio():.4f}")
print(f"OOS RMS α: {rolling.get_oos_rms_pricing_error(X):.6f}")

Compare RP-PCA vs Standard PCA

from rppca import RPPCA

# Standard PCA (γ = -1)
pca = RPPCA(n_factors=5, gamma=-1)
pca.fit(X)

# RP-PCA (γ = 10)
rppca = RPPCA(n_factors=5, gamma=10)
rppca.fit(X)

print(f"PCA    Max SR: {pca.get_max_sharpe_ratio():.4f}")
print(f"RP-PCA Max SR: {rppca.get_max_sharpe_ratio():.4f}")  # typically 2× higher

Functional API

from rppca import rppca_decompose, pca_decompose

# RP-PCA
loadings, factors, eigenvalues = rppca_decompose(X, n_factors=5, gamma=10)

# Standard PCA (convenience wrapper)
loadings, factors, eigenvalues = pca_decompose(X, n_factors=5)

Algorithm

Factor Model

Excess returns follow an approximate factor model:

X = F · Λᵀ + e

where X is T×N (periods × assets), F is T×K (latent factors), Λ is N×K (loadings), and e is the idiosyncratic residual.

RP-PCA Matrix

RP-PCA performs eigendecomposition on a modified second-moment matrix:

S = (1/T) XᵀX + γ · X̄ X̄ᵀ

where X̄ is the N×1 vector of time-series means of each asset.

This is equivalent to PCA on:

(1/NT) Xᵀ (I_T + (γ/T) 𝟏𝟏ᵀ) X

Estimation Steps

Step Operation Output
1 Compute S = (1/T) XᵀX + γ · X̄X̄ᵀ N×N matrix
2 Eigendecompose S, take top K eigenvectors v₁, ..., v_K
3 Loadings: Λ̂ = √N · [v₁ ... v_K] N×K matrix
4 Factors: F̂ = X · Λ̂ · (Λ̂ᵀΛ̂)⁻¹ T×K matrix

Role of γ (Risk-Premium Weight)

Value Effect
γ = -1 Standard PCA on the covariance matrix
γ = 0 PCA on the second-moment matrix (1/T)XᵀX
γ > 0 Overweight the mean → strengthens weak-factor signal
γ = 10 Recommended by the paper for empirical applications

The key insight: a larger γ increases the eigenvalue signal of factors with high Sharpe ratios, making weak-but-important pricing factors detectable.

Signal Strengthening

The population limit of S converges to:

Λ (Σ_F + (1+γ) μ_F μ_Fᵀ) Λᵀ + Var(e)

For PCA (γ=-1), the signal is driven only by variance Σ_F. For RP-PCA (γ>0), the mean μ_F also contributes, boosting weak factors that have high Sharpe ratios (large μ_F relative to Σ_F).

⚠️ Do NOT Demean the Data

Critical: RP-PCA requires the raw (un-demeaned) excess returns.

The key matrix for RP-PCA is:

S = (1/T) XᵀX + γ · X̄ X̄ᵀ

The entire advantage of RP-PCA over standard PCA comes from the mean vector X̄ (the time-series average of each asset's excess returns). If you subtract column means before fitting:

# ❌ WRONG — this destroys the risk-premium signal!
X_demeaned = X - X.mean(axis=0)
model = RPPCA(n_factors=5, gamma=10)
model.fit(X_demeaned)  # gamma has NO effect — identical to PCA!

then X̄ becomes zero, the γ·X̄X̄ᵀ term vanishes, and all γ values produce identical results. RP-PCA degenerates to standard PCA.

# ✅ CORRECT — pass raw excess returns (not demeaned)
model = RPPCA(n_factors=5, gamma=10)
model.fit(X)  # gamma works as intended

Common pitfall: Standard PCA implementations often demean data as a preprocessing step. Do not apply this habit to RP-PCA — the mean IS the signal.

The library will emit a UserWarning if it detects that the input data has been demeaned.

Look-Ahead Bias

⚠️ Critical for quantitative strategies

The Problem

The in-sample RP-PCA estimation uses the full sample to compute:

  • The mean vector X̄ = (1/T) Σ X_t — includes future returns
  • The second-moment matrix (1/T) XᵀX — includes future returns
  • The eigendecomposition — depends on all of the above

This introduces look-ahead bias (未来函数). If you estimate factors at time t using data that includes t+1, t+2, ..., your strategy is peeking into the future.

The Solution

Use RollingRPPCA for any application where causality matters:

from rppca import RollingRPPCA

rolling = RollingRPPCA(
    n_factors=5,
    gamma=10,
    window=240,       # 20-year rolling window (as in the paper)
)
rolling.fit(X)        # Internally: at each t, only uses X[t-240:t]

At each time step t, the rolling estimator:

  1. Estimates loadings from X[t-window : t] only (historical data)
  2. Projects X[t] onto those loadings → out-of-sample factor at t
  3. Never uses any data from t+1 onwards

In-Sample vs Out-of-Sample Summary

RPPCA (in-sample) RollingRPPCA (out-of-sample)
Data used Full sample [1, T] Rolling window [t-w, t]
Look-ahead bias ⚠️ Yes No
Use case Academic research, model evaluation Live trading, backtesting
Sharpe ratio Upward-biased Unbiased
Paper reference Sections 2–6 Section 7

API Reference

Classes

RPPCA(n_factors=5, gamma=10.0, normalize=False)

In-sample RP-PCA estimator.

Method Description
.fit(X) Estimate loadings & factors from full sample
.transform(X_new) Project new data onto estimated loadings
.fit_transform(X) Fit and return in-sample factors
.get_max_sharpe_ratio() Maximum Sharpe ratio of the factors
.get_pricing_errors(X) Per-asset pricing errors (alphas)
.get_rms_pricing_error(X) Root-mean-squared pricing error
.get_idiosyncratic_variance(X) Average unexplained variance
.get_explained_variation(X) Fraction of variance explained

RollingRPPCA(n_factors=5, gamma=10.0, window=240, normalize=False, min_window=None)

Rolling-window estimator — strictly look-ahead-bias free.

Method Description
.fit(X) Run rolling-window estimation
.get_oos_max_sharpe_ratio() Out-of-sample max Sharpe ratio
.get_oos_pricing_errors(X) Out-of-sample pricing errors
.get_oos_rms_pricing_error(X) Out-of-sample RMS α
.get_oos_idiosyncratic_variance(X) Out-of-sample idiosyncratic variance
Attribute Description
.oos_factors_ (T_oos, K) out-of-sample factor estimates
.oos_start_ Index where OOS estimates begin
.loadings_history_ List of loading matrices per step
.sharpe_weights_history_ Max-Sharpe weights per step

Functions

Function Description
rppca_decompose(X, n_factors, gamma=10) Core decomposition (returns loadings, factors, eigenvalues)
pca_decompose(X, n_factors) Standard PCA (γ=-1 shortcut)
max_sharpe_ratio(factors) Maximum Sharpe ratio
pricing_errors(X, factors) Per-asset alphas
rms_pricing_error(X, factors) RMS pricing error
idiosyncratic_variance(X, factors) Average unexplained variance
explained_variation_ratio(X, factors) R² of the factor model

Utilities

from rppca.utils import eigenvalue_ratio_test, variance_signal, eigenvalue_spectrum

# Suggest number of factors (Ahn & Horenstein, 2013)
n_factors, ratios = eigenvalue_ratio_test(X, max_factors=10, gamma=10)

# Variance signal diagnostic (strong vs weak factors)
signals = variance_signal(X, n_factors=5, gamma=10)

# Scree plot data
evals = eigenvalue_spectrum(X, gamma=10, n_top=10)

Examples

Run the simulation example:

python scripts/example_simulation.py

Sample output:

1. IN-SAMPLE COMPARISON
  PCA (γ=-1)            |  Max SR: 0.0799  |  RMS α: 0.001088
  RP-PCA (γ=10)         |  Max SR: 0.5728  |  RMS α: 0.000715

4. LOOK-AHEAD BIAS CHECK
  In-sample Max SR:      0.5728  (uses future data — biased)
  Out-of-sample Max SR:  0.1528  (no look-ahead — unbiased)

Citation

If you use this package in your research, please cite the original paper:

@article{lettau2020estimating,
  title={Estimating latent asset-pricing factors},
  author={Lettau, Martin and Pelger, Markus},
  journal={Journal of Econometrics},
  volume={218},
  number={1},
  pages={1--31},
  year={2020},
  publisher={Elsevier}
}

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rppca-0.1.1.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rppca-0.1.1-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

Details for the file rppca-0.1.1.tar.gz.

File metadata

  • Download URL: rppca-0.1.1.tar.gz
  • Upload date:
  • Size: 23.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for rppca-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e10d6eb1da617d401ab8f249797efab6035a25102aba344e76f4602c1099ed72
MD5 390b189a52f351cdf719e3bcaf945252
BLAKE2b-256 5e93e375fdbfb9213fc7ed0cdce211380208d9d625d70b44feac641d3881bbcd

See more details on using hashes here.

File details

Details for the file rppca-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: rppca-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 18.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for rppca-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 46dfdf8c44d1628f81de7c9478e11d28eb8123cf709554c4322fc7e53957c7bf
MD5 aa2d69e8db89c66ad8e4bdb0d3a352be
BLAKE2b-256 9daf326556222c0f8ec59efb4622deb18ebd46b6d5eba1e18d09d1c760436e6e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page