Replication and extension of Gu, Kelly, and Xiu (2021), "Autoencoder Asset Pricing Models". Princeton FIN 580 Final Project — Liam O'Shaughnessy & John Giess.
We rebuild the conditional autoencoder (CA) framework, extend the test period through 2023, and investigate whether reconstruction error constitutes a tradable anomaly signal.
| Model | R²_total | R²_pred | SR (predictive) | SR (chars-only) |
|---|---|---|---|---|
| IPCA | 6.3% | 0.3% | 2.46 (median) | 1.24 |
| CA0 | 15.5% | 0.4% | 2.25 (median) | 1.24 |
| CA1 | 23.0% | 0.8% | 3.79 (median) | 1.24 |
| CA2 | 25.1% | 0.7% | 2.56 (median) | 1.24 |
| CA3 | 25.0% | 1.3% | 4.85 (median) | 1.24 |
Best model (CA2, K=5, 10-seed ensemble, test 2011–2023):
- Total R² = 25.05%, Predictive R² = 0.65%
- Predictive Sharpe = 2.13 vs characteristics-only benchmark of 1.24
- CAPM alpha = 47.5 bps/month (t = 7.76), market beta = 1.58
- Max drawdown = −25.3% (Sep 2011), mean monthly turnover = 30%
Reconstruction error (anomaly score) produces a monotonic return gradient across quintiles but is not a statistically significant predictor of future returns (NW t = −1.07, p = 0.28).
fin580-autoencoders/
├── run.py # Main pipeline entry point
├── config.py # Hyperparameters and paths
├── requirements.txt
├── submit.sh # Adroit SLURM job script
├── submit_build.sh # SLURM job for data preprocessing
├── plot_comparison.py # Standalone plotting for model comparison
│
├── src/
│ ├── data_loader.py # Panel construction, preprocessing, splits
│ ├── build_missing.py # Fill missing characteristics
│ ├── pull_wrds_parquets.py # Pull CRSP/WRDS data
│ ├── api_caller.py # WRDS API wrapper
│ └── plot_results.py # Figure generation utilities
│
├── train_test_val/
│ ├── models.py # ConditionalAutoencoder architectures (CA0–CA3, IPCA)
│ ├── train.py # Training loop, ensemble, AssetPricingDataset
│ ├── managed_portfolios.py # Characteristic managed portfolio construction
│ ├── evaluate.py # R², Sharpe, pricing errors, chars-only benchmark
│ ├── extensions.py # Plan A (anomaly) and Plan B (ablation) analyses
│ └── backtest.py # Long-short backtest, turnover, factor alphas
│
└── outputs/
├── figures/ # Generated plots
├── tables/ # CSV outputs (model_comparison, backtest, etc.)
└── logs/ # SLURM stdout/stderr logs
Data is not included in this repository and must be sourced independently.
Required:
- CRSP monthly stock returns and firm characteristics via WRDS (
src/pull_wrds_parquets.py) - Chen-Zimmermann open-source characteristics panel (link) — the
signed_predictors_dl_widedataset
Processing:
# Pull from WRDS (requires Princeton WRDS credentials)
python src/pull_wrds_parquets.py
# Build and merge characteristic panel
python src/build_missing.py
# Or if you already have a processed parquet:
# pass --processed path/to/panel_processed.parquet to run.pyThe processed panel should be a parquet file with columns: permno, date, ret, ret_excess, and one column per characteristic (91 in our case). Characteristics are rank-normalized cross-sectionally to [−1, 1] each month during preprocessing.
Sample: 4,221,335 stock-months, 91 characteristics, 1972–2023.
Splits: Train 1972–2005 | Val 2006–2010 | Test 2011–2023.
git clone https://github.com/liamos7/fin580-autoencoders
cd fin580-autoencoders
pip install -r requirements.txtRequirements: Python 3.10+, PyTorch 2.0+, CUDA optional but recommended for the full comparison run.
Additional dependencies used in evaluate.py and backtest.py (not in requirements.txt):
pip install statsmodels scipypython run.py \
--processed data/processed/panel_processed.parquet \
--architecture CA2 \
--K 5 \
--seeds 10python run.py \
--processed data/processed/panel_processed.parquet \
--architecture CA2 \
--K 5 \
--seeds 10 \
--run-extensionspython run.py \
--processed data/processed/panel_processed.parquet \
--architecture CA2 \
--K 5 \
--seeds 10 \
--run-extensions \
--run-backtestpython run.py \
--processed data/processed/panel_processed.parquet \
--compare-all \
--seeds 10 \
--run-backtestpython run.py \
--processed data/processed/panel_processed.parquet \
--architecture CA2 \
--K 5 \
--tune-l1Edit submit.sh to set your paths, then:
sbatch submit.shThe job requests 1 GPU, 4 CPUs, 32GB RAM, and a 12-hour wall time, which is sufficient for the full 10-seed CA2 ensemble plus extensions and backtest.
| Flag | Default | Description |
|---|---|---|
--processed |
None |
Path to preprocessed .parquet; skips raw data build |
--architecture |
CA2 |
Model depth: IPCA, CA0, CA1, CA2, CA3 |
--K |
5 |
Number of latent factors |
--seeds |
10 |
Ensemble size (predictions averaged across seeds) |
--compare-all |
False |
Train all 30 architecture × K configurations |
--run-extensions |
False |
Run Plan A (anomaly) and Plan B (ablation) |
--run-backtest |
False |
Run predictive long-short backtest |
--tune-l1 |
False |
Grid-search L1 penalty on validation set |
The conditional autoencoder follows Gu et al. (2021). For each month
Beta network: maps firm characteristics
Factor network: projects
Predicted return:
Loss: MSE + L1 weight penalty + (optionally) Sharpe cap + factor correlation penalty.
| Architecture | Hidden layers | Parameters (K=5) |
|---|---|---|
| IPCA | 0 (fully linear) | ~470 |
| CA0 | 0 (linear beta net) | ~470 |
| CA1 | [32] | ~3,200 |
| CA2 | [32, 16] | ~3,700 |
| CA3 | [32, 16, 8] | ~3,830 |
After a full run, outputs/tables/ contains:
| File | Contents |
|---|---|
model_comparison.csv |
R², Sharpe for all 30 configurations |
robustness_summary.csv |
Median/IQR of predictive Sharpe across configs |
plan_a_portfolio_sorts.csv |
Quintile returns by anomaly score |
plan_a_hl_significance_anomaly.csv |
NW/t-test/Wilcoxon for anomaly H-L spread |
plan_a_hl_significance_pred.csv |
Same for predicted-return decile spread |
plan_a_transitions.csv |
Transition analysis (low→high anomaly) |
plan_b_ablation.csv |
5×5 energy × disentangle grid |
backtest_performance.csv |
Ann. return, vol, Sharpe, Sortino, drawdown, hit rate |
backtest_monthly_returns.csv |
Month-by-month L/S, long, short, chars, market returns |
backtest_drawdown.csv |
Cumulative return and drawdown time series |
backtest_alphas.csv |
CAPM (and optional FF3) alpha with NW SEs |
backtest_rolling_sharpe.csv |
36-month rolling Sharpe |
backtest_turnover.csv |
Monthly turnover and Jaccard similarity |
backtest_turnover_summary.csv |
Mean/median/std turnover, avg holding period |
backtest_long_holdings.csv |
Most frequently longed stocks by permno |
backtest_short_holdings.csv |
Most frequently shorted stocks by permno |
Tests whether reconstruction error
- Portfolio sorts: quintile sort on anomaly score, examine next-month spread
-
Predictive regression:
$r_{i,t+1} = \alpha + \beta_1 |\varepsilon_{i,t}| + \beta_2 \hat{r}_{i,t+1} + u$ with month-clustered SEs - Transition analysis: stocks moving from low to high anomaly quintile vs. control
Finding: Monotonic return gradient (low anomaly earns more than high anomaly) but spread is not statistically significant (NW t = −1.07). Autoencoder residuals are noise, not mispricing.
5×5 grid over energy penalty
Predictive long-short strategy using rolling 60-month factor mean
Gu, S., Kelly, B., & Xiu, D. (2021). Autoencoder asset pricing models.
Journal of Econometrics, 222(1), 429–450.
Chen, A. Y., & Zimmermann, T. (2022). Open source cross-sectional asset pricing.
Critical Finance Review, 11(2), 207–264.