Work · competition · 4 min read
IMC Prosperity 4
The competition ran for sixteen days over five rounds. Each round we submitted one Python file that traded against the exchange's bots. Our team of three finished 458th of 18,803 teams. The part I'd show is the infrastructure we built to get there.
Simulate the exchange, do not replay it
The competition's markets are made by a few scripted bots around a hidden fair value. Replaying the historical CSVs assumes our orders wouldn't have changed anything, so we reverse-engineered the bots and simulated the market instead. The trick came from the open-source tooling. Submit a trader that buys one unit at the first tick and holds, and the exchange's own P&L log gives away the fair-value path.
A brute-force search over quoting formulas then matched the bots' published quotes exactly, 99.7 to 100% for the two bots on one product and 99.9% and 99.0% on another. A non-obvious finding was that one set of bots quoted proportional offsets, floor(FV × (1 − 3/4000)), instead of fixed ticks. On matched fair-value paths the simulator reproduced the portal's P&L to within 0.8% in total.
The simulator we inherited was a single Rust file. Working with Claude Code, I refactored it into a per-asset plug-in architecture and added the twelve round-3 products. For round 5 I rebuilt it as a two-layer scenario engine. The first layer generates joint fair-value paths that honour the hidden constraints the data analysis had found. Five products summed to exactly 50,000. One pair's sum drifted only slowly, with a within-day standard deviation far below that of either leg. A triplet was tied by a looser weighted linear relation. The second layer is three shared Poisson trade-burst processes across 50 products, because trades arrived in synchronised bursts and not as 50 independent streams. Across those changes the simulator grew from 1,872 to 5,923 lines.
On top of the simulator sat an Optuna optimiser. It splits seeds into train, validation and test sets, retests on fresh seeds at the end of every study, and runs anti-overfitting diagnostics: deflated Sharpe ratio, probability of backtest overfitting by combinatorial cross-validation, cluster stability and fANOVA. Round 3 ran five studies and 2,224 trials. The trader we shipped was the out-of-sample winner of the second study (deflated-Sharpe probability 1.0, overfitting probability 0.03).
A browser workshop for the analysis had thirteen market-microstructure kernels (microprice, queue imbalance, order-flow imbalance, mark-outs by counterparty, effective versus realised spread, lead-lag, pair spreads with half-life) compiled from Rust to WebAssembly, running in a web worker. A panel took 5 to 50 milliseconds on 580,000-row rounds.
Results by round
| Round | Algorithmic P&L | Manual P&L | Position after round |
|---|---|---|---|
| 1 | 99,546 | 87,995 | 432 |
| 2 | 98,726 | 204,355 | 176 |
| 3 (leaderboard reset to zero) | 11,141 | 66,229 | 1,998 |
| 4 | 70,320 | 57,684 | 716 |
| 5 (final) | 66,247 | ≈ 67,081 | 458 |
Five things we killed
- An option-implied drift signal that looked like +4,716 died in a 26-trial random search. Train and holdout uplift were correlated at −0.700, and not one configuration beat the baseline on both.
- A cross-strike spread trade showing +11,265 was a mid-to-mid accounting artefact. With realistic fills no configuration in the threshold sweep survived, and the best one made +20 over three days on three trades.
- Delta-hedged short volatility on the option chain returned +166 to +1,033 a day in walk-forward. Our planning estimates had been +1,320 a round from decay alone, and up to +18,600 if the vouchers expired to intrinsic.
- Counterparty identifiers carried no alpha. All nine integration variants lost, and the standalone version lost 64,044 over three replay days.
- In round 5 a "pulse tilt" overlay did nothing. We knew because three submissions came back with byte-identical trade histories, so the final trader shipped with it disabled.
There's also one lesson about the tooling. The portal's 1,000-tick preview backtest is dominated by price-path luck. Our round-3 trader showed +17,449 there and made +11,141 in the 10,000-tick final, and the first thousand ticks of the final contributed +817.
Credits
We were a team of three, and I wrote about three quarters of the commits. My parts were the simulator refactor and its round-3 and round-5 calibrations (my teammate contributed the round-5 triplet factor model), the Rust/WASM analysis workshop, the optimiser framework, the main trader line for rounds 1 to 3, and the manual-challenge solutions. My teammate led the round-1 bot calibration, the round-4 shipped trader and much of round 5. The backtester and visualiser skeleton came from an open-source release by another team, which we adapted and credit in the repository. All of the Rust and WebAssembly code was written with Claude Code.