Portfolio allocation under a Continuous-Time Markov Decision Process (CTMDP) regime model with Jump-Diffusion dynamics, a Late-Fusion neural filter, and a Residual LSTM corrector. Built with a strict no-look-ahead discipline.
Research tool, not investment advice. Live execution (Alpaca) is gated behind explicit confirmation and defaults to paper trading.
Static mean-variance allocation ignores that markets switch between regimes (calm / stressed) with fat-tailed, jumpy returns. This project infers the latent regime online and allocates per regime, then blends — explicitly modelling jumps (Poisson-Normal mixture) instead of assuming Gaussian returns, while guaranteeing no information leaks from the future into any training or scaling step.
- Prices & macro: daily OHLCV for ~17 liquid US ETFs/stocks (SPY, QQQ, sector
ETFs, Treasuries, commodities, single names) from
yfinance, 2010–2026, cached to Parquet underoutputs/cache/. Macro series (VIX, ^TNX, DXY) are shifted one trading day at the loader to avoid look-ahead. - Fundamentals: P/E, P/B, EPS and quarterly fundamentals (
data/fundamentals.py), applied with a reporting lag so only already-published data is used. - Evaluation is walk-forward out-of-sample: ~1,856 OOS daily observations (~7.4 years) across folds.
Per-asset regime-conditioned Jump-Diffusion log-returns:
dX_i^k(t) = mu_i^k dt + sigma_i^k dW_t + J_i^k dN_t(lambda_k)
The regime k follows a CTMDP whose discrete-time posterior
p_k(t) = P(z_t = k | F_t) is approximated by the Late-Fusion neural filter
trained against the Jump-Diffusion mixture log-likelihood. A Residual LSTM adds a
small logit-space correction p_corrected = softmax(logits_filter + delta_LSTM).
The portfolio is per-regime CRRA optimisation with James-Stein shrinkage and L2
entropy regularisation, blended pi_t = sum_k p_k(t) pi_k*, projected to the
simplex and risk-scaled to a target volatility.
configs/ data/(loader,splits,fundamentals) features/(windows,standardize,macro)
models/(filter,jump_diffusion,ctmdp,trainer) optimization/portfolio
backtest/(walk_forward,metrics,statistical_tests) evaluation/ execution/(alpaca)
notification/ scripts/ tests/
Walk-forward OOS, 1,856 days aggregated across folds. Strategy metrics from
outputs/metrics/aggregate_summary.csv; baselines reconstructed on the same OOS
dates via python -m scripts.baseline.
| Series | Sharpe | Vol | Return | Max DD |
|---|---|---|---|---|
| JumpDiffPortfolio (net) | 0.76 | 15.4 % | 17.0 % | −16.5 % |
| SPY (buy & hold) | 0.40 | 17.4 % | 11.7 % | −35.7 % |
| Equal-weight | 0.37 | 13.9 % | 9.6 % | −29.3 % |
On point estimates the strategy beats both passive baselines on all three axes — higher Sharpe, higher return, and roughly half the drawdown (−16.5 % vs −36 %/−29 %). Sortino 1.13, Calmar 1.03, hit rate 55.4 %.
Honest significance check. Diebold-Mariano (MSE) vs the benchmark is significant (p = 0.045), but the White Reality Check p = 0.20 is NOT significant once data-snooping is accounted for, and the Sharpe 95 % CI is wide ([0.32, 1.71]). Read this as promising, not proven: the out-performance is real in-sample-of-folds but the edge is not established at the strict multiple-testing bar.
Python 3.12+ · NumPy · SciPy · pandas · PyTorch (filter + LSTM) · yfinance ·
Alpaca (execution) · pytest. CRRA optimisation via SLSQP (scipy.optimize).
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # rellena credenciales solo si vas a ejecutar/notificar
# Full training + walk-forward backtest (genera todas las figuras del paper)
python -m scripts.train_backtest --folds 6 --epochs 100 --lstm-epochs 80
python -m scripts.infer_daily # inferencia diaria (sin ejecución)
python -m scripts.diagnose # diagnósticos de sesgo (correlación vs factores)
pytest -q # 34 testsAPCA_API_KEY_ID, APCA_API_SECRET_KEY, APCA_BASE_URL (paper por defecto),
CONFIRM_LIVE_TRADING (debe valer yes para trading en vivo), TELEGRAM_BOT_TOKEN,
TELEGRAM_CHAT_ID. Se leen solo del entorno; nunca hay claves en el código.
- Train/Val/Test split before any standardization or scaling.
- Macro data shifted by one trading day (
shift(1)) at the loader. - Sliding windows are causal (use only
t-w, …, t-1). - The rolling standardizer fits only on the train fold.
- Per-fold artifacts are saved only after walking forward.
tests/test_no_leakage.py+tests/test_core_math.pyenforce these invariants.