Skip to content

Repository files navigation

QuantFin Exeter — AI Investment Challenge 2025-26

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.apphttp://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.

Project at a glance

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

Table of contents

  1. Project idea
  2. Implementation overview
  3. Quick start (for judges)
  4. How to obtain data
  5. Repository layout
  6. Running the web application
  7. Training and evaluation
  8. Environment variables
  9. AI tools disclosure
  10. Licence

1. Project idea

The project focuses on algorithmic execution, not alpha forecasting. The objective is to split a parent order into child orders over $T$ intervals while minimizing trading cost relative to a benchmark execution price.

We optimize implementation shortfall (IS) in basis points:

$$ \mathrm{IS}_{\text{bps}} = 10^4 \cdot \text{side} \cdot \frac{\bar{p}_{\text{arrival}} - \bar{p}_{\text{fill}}}{\bar{p}_{\text{arrival}}} $$

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.

2. Implementation overview

End-to-end flow in this repository:

  1. Build/load daily feature panels (features_train/val/test.parquet).
  2. Optionally enrich with BBO order-imbalance and news intensity.
  3. Detect market regimes (HMM with robust fallback).
  4. Run execution episodes in a Gymnasium environment with impact and risk penalties.
  5. Train/evaluate PPO and compare against TWAP/VWAP/Almgren-Chriss/Immediate.
  6. 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.

3. Quick start (for judges)

Requirements

  • Python 3.11 (Torch / SB3 wheels may fail on newer versions)
  • Git, ~4 GB disk (venv + data)

Setup

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

Configure data path

# 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.

Run

# Web application (recommended — includes Case Study, Execution Lab, User Manual)
python -m web.app
# Open http://localhost:5001

# Quick smoke test
pytest -q

The 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).


4. How to obtain data

All data sources are publicly accessible per competition rules (Rule 4.6). No proprietary terminals required.

4.1 Stock price data (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.

4.2 VIX data (included in features or downloadable)

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.

4.3 NASDAQ ITCH BBO order imbalance (optional, recommended)

Source: Databento — NASDAQ TotalView-ITCH BBO-1m schema.

How to obtain:

  1. Go to databento.com/portal/browse
  2. Register for a free account — new accounts receive $125 USD free credit
  3. Search for NASDAQ TotalView-ITCH, select BBO-1m schema
  4. Purchase data for SPY from 2019-01-02 to 2024-12-31 (optionally also AAPL)
  5. 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.parquet

Coverage starts 2019-01-02; earlier rows use neutral OBI (0). The pipeline merges this automatically when bbo_daily.parquet exists.

4.4 News sentiment counts (optional)

Source: Finnhub — free tier, no credit card required.

  1. Register at finnhub.io/register to get a free API key
  2. Add FINNHUB_API_KEY=your_key to .env
  3. 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 20

Writes data/processed/news_daily_SPY.parquet. The RL observation includes z-scored daily news intensity when this file exists.

4.5 LLM governance (Anthropic Claude)

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_key to .env. Model: claude-sonnet-4-20250514.
  • Without an API key, an offline template fallback generates and caches deterministic explanations.

5. Repository layout

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)

6. Running the web application

python -m web.app
# Starts on http://localhost:5001

A 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

7. Training and evaluation

Quick eval (no training)

python scripts/train.py --ticker SPY --order-notional-usd 5e6

Eval-only loads models/best_ppo_twap_gap.zip automatically when --load-model is omitted and that file exists.

Train PPO

python scripts/train.py --ticker SPY --train --order-notional-usd 5e6 \
  --residual-bound 0.15 --relative-is-scale 2.0 --timesteps 300000

Saves models/best_ppo_twap_gap.zip when the path-aligned TWAP gap improves during training.

Key training flags

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.

Scenario benchmarks

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.0

Path-aligned evaluation

train.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

8. Environment variables

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)

9. AI tools disclosure

Per competition Rule 4.1 (Transparency) and Rule 4.5 (Disclosure):

AI as development assistant

  • 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.

AI as component of the solution

  • 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 in data/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.

Data created as part of the solution

  • 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.

10. Licence

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)

About

Repository for CFA AI Investment Challenge competition, by Team 18 (QuantFin Exeter)

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages