IMC Prosperity 4: placing 78th overall
Our team, I AM SEE, finished 78th overall and 25th in the US in IMC Prosperity 4 — 18,803 teams, top 0.26%. Algorithmic rank was 60th, manual was 1,181st, and we ended with 655,112 XIREC.
This is a round-by-round recap of what we built, what actually made money, and where the bot leaked PnL.

What Prosperity is
Prosperity is IMC's yearly algorithmic trading contest. Each round adds new products plus a manual puzzle. Your Python trader runs against a mix of market makers and other bots, and the scoreboard is seashells (PnL). On every tick you see the book and decide what to send.
You get a handful of public days to tune against. The real run is a hidden day you never see in advance. Ship the bot, then wait.
There are two phases. Rounds 1 and 2 are qualifiers: 200k combined seashells to advance. The board zeros out at round 3, and only rounds 3–5 count for final placement.
Round 1 (qualifier)
Algo products: ash-coated osmium (ASH_COATED_OSMIUM), intarian pepper root (INTARIAN_PEPPER_ROOT)
Manual: an exchange auction. Bid on dryland flax and ember mushrooms that the merchant guild later buys back at fixed prices ($30/unit flax, $20/unit mushrooms, $0.10/unit fee). The job is to estimate the clearing price and send the right limit.
Qualifier PnL: $95,271 algo · $87,995 manual. These numbers do not count toward the final ranking. They just had to get us over 200k with round 2.
Round 2 (qualifier)
Same products as round 1, with a one-shot market-access bid. Pay a fee, and if you land in the top 50% of bids you get 25% more order-book flow. The fee comes out of round 2 profits.
Manual: split a 50,000 XIREC budget across three outpost growth pillars.
Qualifier PnL: $93,849 algo · $24,233 manual. Combined with round 1 we were safely into phase 2.
Round 3
Products: velvetfruit extract (VEX), hydrogel pack, and ten European calls on VEX with strikes 4000–6500.
This was the first round with actual options. The calls had fixed strikes, fixed expiry, and cash-settled into seashells. Limits were 200 on the commodities and 300 on each option. Algo PnL $9,759.91 · manual $75,410.
Algorithm
The hard part was quoting options without getting picked off. Sticking quotes around a Black–Scholes value falls apart quickly: you do not know the right vol, and the informed bots in the sim know more than you.
We anchored fair value to the outer walls of the book — the deepest resting bid and ask — instead of the inside spread. For ITM strikes (4000–5200) those walls move slowly and sit closer to equilibrium than the noisy best bid/ask.
# quote off the outer walls, not the inside spread
wall_bid = min(depth.buy_orders)
wall_ask = max(depth.sell_orders)
wall_mid = (wall_bid + wall_ask) / 2
orders.append(Order(symbol, int(wall_mid) - 1, buy_size))
orders.append(Order(symbol, int(wall_mid) + 1, -sell_size))On top of that we stacked three more pieces:
Last-print capture. If the most recent trade was still inside the live spread, we posted there and waited for the same print to happen again. Pure liquidity — we did not cross — but it harvested repeat flow.
Rolling z-score fade. Each option kept an 800–1500 tick mid history (longer for deeper OTM, where moves are rarer). Past about ±2.2 to ±2.5 sigma we faded 4–10 lots. ATM mids mean-reverted hard during the day.
if len(price_history) >= window:
mean = sum(price_history[-window:]) / window
variance = sum((x - mean) ** 2 for x in price_history[-window:]) / window
std = variance ** 0.5
z = (current_mid - mean) / std if std > 0 else 0.0
if z < -Z_LO:
orders.append(Order(symbol, ask, revert_size))
elif z > Z_HI:
orders.append(Order(symbol, bid, -revert_size))Slow EMAs on VEX and hydrogel. Alphas around 0.001–0.002 as a fair-value proxy, size 50 when price stretched. On VEX we also used top-of-book imbalance: heavy bid pressure meant we pulled our offers, and the other way around, so we were less likely to get run over on the passive side.
The round was slightly green, not great. The z-score fade was real. Wall market-making worked if we treated the deep ITM contracts (4000/4500) more like delta-1 than options. We were still leaving money on the table by fading through directional VEX moves. That is what round 4 fixed.
Round 4
Same universe as round 3. The hidden day was a different animal: VEX trended instead of chopping, and our round 3 bot was a mean-reversion machine. It got hit early. Algo PnL $85,393.35 · manual $0 — about 8.75× the round 3 algo number.
Algorithm
We added a dual-EMA trend filter on VEX:
FAST_ALPHA = 0.015
SLOW_ALPHA = 0.0015
fast_ema = fast_ema + FAST_ALPHA * (mid - fast_ema)
slow_ema = slow_ema + SLOW_ALPHA * (mid - slow_ema)
trend = fast_ema - slow_emaThen we labeled regimes from both the gap versus an anchor (5250) and that trend:
VEX_ANCHOR = 5250
dev = slow_ema - VEX_ANCHOR
if dev >= 45 and trend <= 8:
regime = -1 # extended high, momentum fading
elif dev <= -45 and trend >= -24:
regime = +1 # extended low, momentum fading
else:
regime = 0The important bit is the asymmetry. We only fade after momentum weakens. If price is high and still accelerating, we stay out. No buying the first dip of a waterfall.
Those labels capped option inventory:
| regime | long cap | short cap | bias |
|---|---|---|---|
| bearish (−1) | 120 | 20 | stay short |
| neutral (0) | 300 | 20 | balanced |
| bullish (+1) | 300 | 0 | stay long |
| bear trend | 120 | 20 | fade longs |
| bull trend | 300 | 0 | fade shorts |
When conviction was high we also took directional option flow at 30 lots instead of waiting for the snapback.
Z-score reversion and wall quoting from round 3 stayed underneath. The regime layer just decided which of those trades were allowed. Almost all of the jump in PnL came from not mean-reverting into a trend.
Round 5
Products: 50 equity-like names in 10 families of five (galaxy sounds, microchip, oxygen shake, pebbles, robot, sleep pod, translator, snackpack, panel, UV visor).
This was the largest round and the most interesting to design for. No options this time — 50 spots, 10-lot caps, 500 lots of total capacity, about a million ticks. Algo PnL $415,534.45 · manual $69,015 — 4.9× round 4 and ~42× round 3.
Algorithm
The edge was in the product design. Names inside a family moved together and their spreads mean-reverted. We hunted those relationships, traded them, and market-made everything that did not have a cleaner signal.
Five layers, highest priority first each tick:
Layer 0 — at-touch market making. Default for anything a stronger layer had not claimed. Passive bid at best_bid, passive ask at best_ask, size from inventory:
buy_size = max(0, LIMIT - position)
sell_size = max(0, LIMIT + position)
if buy_size > 0:
orders.append(Order(symbol, bid, buy_size))
if sell_size > 0:
orders.append(Order(symbol, ask, -sell_size))We tried tightening into the spread (bid+1 / ask-1 when the spread was wider than 2). That lost about $71k versus at-touch across the three public days. On these names, adverse selection is wide enough that you need the whole spread to pay for the bad fills. We also turned MM off on SLEEP_POD_LAMB_WOOL, PANEL_1X2, and SNACKPACK_VANILLA — they lost money at the touch on every sample day. Directional layers could still touch them.
Layer 1 — book imbalance lean. Before posting, look at displayed size:
bid_vol = sum(depth.buy_orders.values())
ask_vol = sum(-v for v in depth.sell_orders.values())
imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol)
if imbalance > 0.05:
cancel_passive_buy
if imbalance < -0.05:
cancel_passive_sellIf the book is already piled on the bid, we do not join that queue. Small, consistently green.
Layer 2 — short-horizon drift fade. Last five mids. If the 5-tick move is past ±150, lean against it:
if len(mid_history) > DRIFT_LB:
drift = mid_history[-1] - mid_history[-DRIFT_LB - 1]
if drift > 150:
orders.append(Order(symbol, bid, -sell_size))
elif drift < -150:
orders.append(Order(symbol, ask, buy_size))In practice this almost only fired on PEBBLES_XL, which had bot jumps big enough to trip it. OXYGEN_SHAKE_CHOCOLATE got a tighter version (100 points over 5 ticks, 7 lots) because that name reverted on a shorter horizon than the rest.
Layer 3 — EWMA pair spreads. The most reliable structural layer. Online EWMA of each spread, trade only at extreme z:
PAIR_ALPHA = 0.001
PAIR_Z_THR = 3.0
PAIR_TAKE = 3
d = spread - ewma_mean
ewma_mean += PAIR_ALPHA * d
ewma_var = (1 - PAIR_ALPHA) * ewma_var + PAIR_ALPHA * d * d
z = d / (ewma_var ** 0.5)
if z > PAIR_Z_THR:
pair_signal[a] -= 1
pair_signal[b] -= 1
elif z < -PAIR_Z_THR:
pair_signal[a] += 1
pair_signal[b] += 1Seven sum pairs (a + b stationary) and two diff pairs (a − b stationary):
ROBOT_MOPPING + ROBOT_VACUUMINGTRANSLATOR_GRAPHITE_MIST + TRANSLATOR_SPACE_GRAYPEBBLES_XL + PEBBLES_XSOXYGEN_SHAKE_CHOCOLATE + OXYGEN_SHAKE_MINTSNACKPACK_RASPBERRY + SNACKPACK_VANILLASLEEP_POD_COTTON + SLEEP_POD_LAMB_WOOLGALAXY_SOUNDS_DARK_MATTER + GALAXY_SOUNDS_SOLAR_FLAMESROBOT_DISHES − ROBOT_MOPPINGSNACKPACK_CHOCOLATE − SNACKPACK_PISTACHIO
PAIR_ALPHA = 0.001 is roughly a 1,000-tick lookback. A z-threshold of 3.0 is picky on purpose. That layer was green on all three sample days.
Layer 4 — panel size spreads. The cleanest relationship in the set. Panel prices scale with physical size, so a 2×4 is not independent of a 1×4. We fit two regressions and then rounded the coefficients:
PANEL_SPREADS = (
("PANEL_1X2", "PANEL_1X4", -0.22, 11000.0, 560.0),
("PANEL_2X2", "PANEL_4X4", -0.73, 16800.0, 590.0),
)
z = (mids[left] - beta * mids[right] - mean) / std
if z > 1.0:
side = -1
elif z < -1.0:
side = +1
if abs(z) < 0.25:
side = 0Exact fitted decimals from three days are a gift to overfit. The rounded betas cost under 2k in sample and held up. The panel book was worth 30k+ per public day.
Layer 5 — all-day directional holds. For nine names we just sat at the cap:
| product | target | why |
|---|---|---|
GALAXY_SOUNDS_BLACK_HOLES | +10 | drifted up every sample day |
MICROCHIP_OVAL | −10 | drifted down every sample day |
OXYGEN_SHAKE_GARLIC | +10 | consistent daily trend |
PEBBLES_XS | −10 | family-sum: XS kept selling off |
ROBOT_VACUUMING | −10 | persistent short in sample |
SLEEP_POD_LAMB_WOOL | +10 | green all three days even though MM lost |
UV_VISOR_AMBER | −10 | persistent short |
UV_VISOR_MAGENTA | +10 | persistent long |
UV_VISOR_RED | +10 | persistent long |
A few more names used the open: below a cutoff go long, above go short. We only kept rules that survived leave-one-day-out on the three public days. Twenty-five open-price rules on three days is how you overfit a hidden test.
What actually paid
Panel spreads were the most honest edge — size ratios are a real constraint, not a pattern that can vanish overnight. EWMA pairs were next, especially pebbles, where the five names in the family sum to something close to a constant.
Directional holds printed the most raw PnL and also the most variance, which is what you want when you step off pure structure. Three public days is a thin sample; some of those bets were always going to miss on the hidden day.
At-touch MM was the floor. It ran on every name without a stronger signal and made money by being the quote.
The numbers
Qualifier (phase 1, not in the final rank):
| round | algo | manual |
|---|---|---|
| 1 | $95,271 | $87,995 |
| 2 | $93,849 | $24,233 |
Phase 2 (this is the ranking):
| round | algo | manual |
|---|---|---|
| 3 | $9,759.91 | $75,410 |
| 4 | $85,393.35 | $0 |
| 5 | $415,534.45 | $69,015 |
| total | $510,687.71 | $144,425 |
Final score: $655,112. 78th / 18,803 globally, 25th in the US, algorithmic 60th, manual 1,181st.
What I took away
Mean reversion was the engine. Option mids in rounds 3–4, then drift fades, pair books, and panel ratios in round 5. Same idea, different anchors. If the anchor is real, waiting is the trade.
Knowing when not to fade mattered more than a fancier quote. Almost all of the round 3 → 4 jump was a regime switch, not better parameters.
Structure beat mined correlations. Panel size and the pebbles family sum are properties of the product. Those were the trades I trusted.
Tighter quotes were not free money. Inside-spread MM lost. When the informed flow is wide, you need the full spread to survive the fills you do not want.
We threw out a lot of ideas that looked great in sample and failed leave-one-day-out. That hurts the backtest and is usually the right trade when you only get one hidden day.
I would run it again.
