University of Exeter · Team 18
| Kirill Papka | Thomas Nguyen | Harrison Maxwell | Maksim Kitikov |
|---|
Institutional optimal trade execution using regime-aware reinforcement learning (PPO), classical benchmarks (TWAP / VWAP / Almgren–Chriss / Immediate), NASDAQ ITCH order-book imbalance, and an LLM governance layer (Anthropic Claude, cached for reproducibility).
Live demo: https://quantfin.dev/execution · locally: python -m web.app → http://localhost:5001
This repository contains the full research-to-demo stack: feature engineering, regime detection, an execution RL environment, benchmark strategies, evaluation scripts, and a web interface for judges.
Cloud deployment notes: DEPLOY.md.
Problem: execute large buy/sell orders with lower implementation shortfall than static schedules.
Idea: blend market regime awareness with a constrained RL policy that adapts around TWAP, then benchmark on the same execution windows for fair comparison.
Why this is useful: practitioners can compare transparent classical baselines and adaptive RL behavior, with an LLM explanation layer for governance and auditability.
What is included:
- Data pipeline for market, optional BBO imbalance, and optional news features
- Regime detection and trend conditioning
- Execution environment with realistic constraints/impact
- PPO training/evaluation against TWAP, VWAP, Almgren-Chriss, Immediate
- Flask web app with case study and interactive execution lab
- Project idea
- Implementation overview
- Quick start (for judges)
- How to obtain data
- Repository layout
- Running the web application
- Training and evaluation
- Environment variables
- AI tools disclosure
- Licence
The project focuses on algorithmic execution, not alpha forecasting. The objective is to split a parent order into child orders over
We optimize implementation shortfall (IS) in basis points:
where side = +1 for sells and side = -1 for buys.
Core hypothesis:
- Market conditions are non-stationary, so one fixed schedule is often suboptimal.
- A regime-aware RL policy can improve execution quality by adjusting participation around a TWAP anchor.
- Decisions should remain interpretable, so we add a governance layer that explains behavior in plain language.
End-to-end flow in this repository:
- Build/load daily feature panels (
features_train/val/test.parquet). - Optionally enrich with BBO order-imbalance and news intensity.
- Detect market regimes (HMM with robust fallback).
- Run execution episodes in a Gymnasium environment with impact and risk penalties.
- Train/evaluate PPO and compare against TWAP/VWAP/Almgren-Chriss/Immediate.
- Surface results in a Flask app and case study report.
Implementation principles used throughout:
- Path-aligned evaluation: RL and benchmarks are compared on identical
(row_start, T)windows. - Constrained actions: PPO learns bounded residuals around TWAP for stability.
- Reproducibility: fixed eval starts and cached LLM outputs are stored in-repo.
- Python 3.11 (Torch / SB3 wheels may fail on newer versions)
- Git, ~4 GB disk (venv + data)
git clone https://github.com/KirikPapka/quantfin_exeter.git
cd quantfin_exeter
python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt# Optional: point to the folder that contains features/ (see section 4)
export CFA_DATA_ROOT="/absolute/path/to/your/data"If omitted, the code auto-detects ./deploy_data when present; legacy fallback is an external CFADATA directory. You can also copy .env.example → .env and set CFA_DATA_ROOT there.
# Web application (recommended — includes Case Study, Execution Lab, User Manual)
python -m web.app
# Open http://localhost:5001
# Quick smoke test
pytest -qThe web app pre-computes the case study at startup (~10 s) and requires a trained PPO checkpoint under models/ (see section 7 or use the provided best_ppo_twap_gap.zip if included).
All data sources are publicly accessible per competition rules (Rule 4.6). No proprietary terminals required.
Source: any free stock API (e.g. Yahoo Finance via yfinance, Alpha Vantage, Polygon free tier, or a CRSP educational extract).
The pipeline expects per-split parquet files under $CFA_DATA_ROOT/features/:
features/
├── features_train.parquet (2018-01-01 to 2022-12-31)
├── features_val.parquet (2023-01-01 to 2023-12-31)
└── features_test.parquet (2024-01-01 to 2024-12-31)
Parquet schema matches load_features_parquet in src/data_pipeline.py (that function is the source of truth).
| Column | Required | Description |
|---|---|---|
date |
Yes | Trading date (indexed after load) |
prc |
Yes | Closing price |
high, low |
Yes | Daily high / low |
vol |
Yes | Daily volume (shares) |
real_vol_20d |
Yes* | 20-day realised volatility (annualised, CRSP-style) |
realised_vol_20 |
Yes* | Alias for the same series if your extract already uses this name |
oprc |
No | Opening price (preferred name in loader) |
openprc |
No | Alias for opening price; if neither oprc nor openprc is present, open defaults to close |
amihud_20d |
No | Amihud illiquidity; else amihud_daily; missing values filled with 0 in-panel |
vix |
No | VIX level (forward-filled into vix_aligned) |
ticker |
No | Filter to one symbol (e.g. SPY) when present |
permno |
No | Security identifier |
shrout |
No | Shares outstanding |
ret, retx |
No | Total & ex-dividend return |
*Exactly one of real_vol_20d or realised_vol_20 must be present.
The loader then builds: realised_vol_20 (internal), sigma_daily, amihud_illiquidity, bid_ask_proxy, volume_to_spread, vix_aligned.
Source: Cboe VIX historical data — free CSV download, 1990–present. The pipeline expects a vix column in the parquet or merges it from a separate file.
Source: Databento — NASDAQ TotalView-ITCH BBO-1m schema.
How to obtain:
- Go to databento.com/portal/browse
- Register for a free account — new accounts receive $125 USD free credit
- Search for NASDAQ TotalView-ITCH, select BBO-1m schema
- Purchase data for SPY from 2019-01-02 to 2024-12-31 (optionally also AAPL)
- Download the CSV and place under
$CFA_DATA_ROOT/raw/
Build the daily panel:
python scripts/build_bbo_daily.py --symbols SPY
# writes data/processed/bbo_daily.parquetCoverage starts 2019-01-02; earlier rows use neutral OBI (0). The pipeline merges this automatically when bbo_daily.parquet exists.
Source: Finnhub — free tier, no credit card required.
- Register at finnhub.io/register to get a free API key
- Add
FINNHUB_API_KEY=your_keyto.env - Fetch news:
python scripts/fetch_finnhub_news.py --symbol SPY
# or for ETF holdings-weighted proxy:
python scripts/fetch_finnhub_news.py --symbol SPY --etf-proxy --max-constituents 20Writes data/processed/news_daily_SPY.parquet. The RL observation includes z-scored daily news intensity when this file exists.
Cost to reproduce: < 20 USD (well within the competition threshold).
- Cached responses are committed in
data/cached_llm/*.json— judges can reproduce all governance text without any API key or cost. - For live calls: add
ANTHROPIC_API_KEY=your_keyto.env. Model:claude-sonnet-4-20250514. - Without an API key, an offline template fallback generates and caches deterministic explanations.
quantfin_exeter/
├── README.md
├── LICENSE # MIT License (competition requirement)
├── requirements.txt
├── environment.yml
├── .env.example # copy to .env, fill in keys
├── web/
│ ├── app.py # Flask routes + API
│ ├── precompute.py # Case study pre-computation
│ ├── export.py # Static site export for the Cloudflare deploy
│ ├── templates/ # Jinja2: home, case study, run, user manual
│ └── static/ # CSS, JS (Plotly charts)
├── data/
│ ├── raw/ # Large files (gitignored)
│ ├── processed/ # bbo_daily.parquet, news (gitignored)
│ └── cached_llm/ # Committed JSON caches for judges
├── deploy_data/
│ └── features/ # Local train/val/test parquet splits (default when CFA_DATA_ROOT is unset)
├── notebooks/
│ └── main_notebook.ipynb # Narrative + figures
├── scripts/
│ ├── train.py # Regime → env → eval [→ PPO train]
│ ├── scenario_benchmarks.py # Controlled flat/up/down scenarios
│ ├── build_bbo_daily.py # BBO CSV → daily OBI parquet
│ ├── fetch_finnhub_news.py # Finnhub → daily news counts
│ └── llm_demo.py # Governance demo
├── src/
│ ├── data_pipeline.py # Parquet load + BBO/news merge
│ ├── regime_detector.py # HMM (+ vol fallback)
│ ├── trading_env.py # Gymnasium optimal execution env
│ ├── rl_agent.py # SB3 train / evaluate
│ ├── benchmarks.py # TWAP, VWAP, A-C, Immediate
│ ├── execution_impact.py # Participation-style market impact
│ ├── trend_classifier.py # Rolling return trend labels
│ ├── regime_switching.py # Trend-based policy routing
│ ├── llm_explainer.py # Claude governance + cache
│ ├── ui_rollout.py # Single-episode rollout for UI
│ └── utils.py
├── models/ # Includes best_ppo_twap_gap.zip + fixed_eval_starts.json (other checkpoints may be gitignored)
├── tests/
└── logs/ # TensorBoard (gitignored)
python -m web.app
# Starts on http://localhost:5001A static export of the same app is hosted at quantfin.dev/execution (Cloudflare, deployed on every push to main, see DEPLOY.md). The hosted Execution Lab serves precomputed results for horizons 5/10/20 on the test split; run locally for arbitrary configurations.
Pages:
| URL | Description |
|---|---|
/ |
Home — project overview and pipeline diagram |
/case-study |
Pre-computed SPY sell execution walkthrough with benchmark results |
/run |
Interactive Execution Lab — configure and run the full pipeline |
/user-manual |
Methodology, formulas (KaTeX), and configuration reference |
python scripts/train.py --ticker SPY --order-notional-usd 5e6Eval-only loads models/best_ppo_twap_gap.zip automatically when --load-model is omitted and that file exists.
python scripts/train.py --ticker SPY --train --order-notional-usd 5e6 \
--residual-bound 0.15 --relative-is-scale 2.0 --timesteps 300000Saves models/best_ppo_twap_gap.zip when the path-aligned TWAP gap improves during training.
| Flag | Default | Purpose |
|---|---|---|
--order-notional-usd |
0 | USD order size (>0 enables physical mode) |
--residual-bound |
0.15 if physical* | TWAP-residual action (e.g. 0.15 = ±15% from TWAP) |
--relative-is-scale |
2.0 if physical* | Per-bar improvement-over-TWAP reward scaling |
--timesteps |
300000 | Training steps |
--n-envs |
1 | Vectorized rollouts |
--eval-freq |
25000 | Evaluate & save best checkpoint every N steps |
*Applied automatically when --order-notional-usd > 0 and the flags are omitted.
python scripts/scenario_benchmarks.py \
--model models/best_ppo_twap_gap.zip \
--order-notional-usd 5e6 --T 10 \
--residual-bound 0.15 --relative-is-scale 2.0train.py and the Case Study reuse models/fixed_eval_starts.json when present, so RL and benchmark comparisons are path-aligned on the same (row_start, T) windows. If it is missing, train.py generates it once.
For head-to-head RL vs TWAP, the report includes path-aligned metrics:
| Metric | Meaning |
|---|---|
mean_RL_minus_TWAP_bps |
Mean IS gap on matched windows (> 0 = RL wins) |
pct_beat_TWAP_IS |
Share of episodes where RL IS > TWAP IS |
IS-gap Sharpe |
Sharpe ratio of the per-episode IS gap |
Copy .env.example → .env:
| Variable | Required | Purpose |
|---|---|---|
CFA_DATA_ROOT |
Yes | Folder containing features/ parquet splits |
ANTHROPIC_API_KEY |
No | Live Claude calls (cached responses work without) |
ANTHROPIC_MODEL |
No | Override model (default: claude-sonnet-4-20250514) |
FINNHUB_API_KEY |
No | News data fetch (free registration) |
Per competition Rule 4.1 (Transparency) and Rule 4.5 (Disclosure):
- Cursor IDE with Claude and Composer — used for code writing, debugging, and refactoring across the codebase.
All generated code was reviewed, understood, and can be explained by the team.
- Anthropic Claude (
claude-sonnet-4-20250514) — integrated into the system for LLM governance (explaining execution decisions in plain English for compliance). Called via API at runtime; responses cached indata/cached_llm/*.json. - Reproduction cost: < $20 USD. Cached responses are committed so judges can verify without API cost.
- Without an API key, an offline template generates deterministic explanations automatically.
- Derived features (volatility, Amihud, bid-ask proxy) — computed from public stock data in
data_pipeline.py. - BBO daily order imbalance — aggregated from NASDAQ ITCH BBO-1m (Databento, publicly purchasable) in
build_bbo_daily.py. - News daily counts — fetched from Finnhub free API in
fetch_finnhub_news.py. - LLM cache files — committed JSON responses from Claude governance calls.
Released under the MIT License as required by competition rules (Rule 1.5).
Built for the CFA Institute AI Investment Challenge 2025–26 (CFA Society United Kingdom).
Contact: mk859@exeter.ac.uk (Maksim Kitikov)