Case study · Quantitative ML · Team project

QMML Market Making Hackathon

Nine live rounds of fair-value prediction, quoting and post-quote trading with elimination on negative cash. The interesting part was not the models. It was deciding how far to trust each one.

Result
2nd of 93 teams · 1st on Sortino ratio · simulated competition bankroll £100,000 → £305,727 (+£205,727), £1,153 behind 1st
My part
Prediction models and model selection, strategy design, sizing logic (strategy.py), rounds 1 / 3 / 5 / 7 / 9
Teammate
Kieran Cooke — EDA, feature engineering, Kelly-criterion implementation, rounds 2 / 4 / 6 / 8. Team name: AlphaBetaPhi
When
March–April 2026, hosted by the QMUL Machine Learning Society
Stack
Python, scikit-learn, SciPy, pandas, NumPy, matplotlib
Code
github.com/M-Rodani1/qmml-market-making-hackathon

Two rounds sized big on ordinary linear regression, one sized moderately on 29 data points, and six rounds traded at minimum exposure because the signal was weak or absent.All money figures are simulated competition bankroll, not real capital. Per-round P&L was not recorded in the repository, so this page does not claim which rounds made the money.

The competition

Each round followed the same structure. Teams received a training table of anonymised features and a target — the hidden value of a synthetic stock — plus a single test row to price. Every team submitted a bid and an ask. The tightest spread won the market-maker role; everyone else then saw those quotes and chose whether to buy at the ask or sell at the bid, and in what size (minimum 10 shares). The true value was revealed, P&L was booked, and any team whose cash went negative was eliminated.

That last rule changes the problem. A good point estimate can still lose money through overconfident sizing, a bad uncertainty estimate, or an unfavourable role in the mechanism. Our working objective became: estimate fair value, quantify how much to trust it, avoid structurally bad trades, and concentrate risk only where the data justified it.

Decision pipeline

  1. Round datafeatures + hidden target, one test row
  2. EDA and featurescorrelations, residuals, influence
  3. Candidate modelslinear, ridge, boosting, mean baseline
  4. Fair value + uncertaintyprediction, CV RMSE, sample size
  5. Observe MM quotesafter our deliberately wide quote loses
  6. Edge outside spread?buy at ask, sell at bid, or minimum
  7. Sizetier or Kelly fraction under survival caps

Prediction generated the possible edge; risk management decided whether that edge was worth betting on.

Avoiding the market-maker role

We submitted the same quote every round — bid 80, ask 400 — so that another team would almost always post the tightest spread. Becoming the market maker meant providing liquidity to dozens of teams who could each trade against your quote after seeing it; a mispriced quote is picked off from both sides. Quoting wide cost nothing under the rules and bought us the option to observe the winning quotes before committing, then trade only when our fair value sat outside the spread by a margin the model's error supported.

Model per stock

The nine datasets behaved very differently, so each got its own model selection rather than one pipeline applied nine times. The table is the output of strategy.py, the pre-round decision engine.

Selected model, evidence and sizing tier per stock
StockRowsFeaturesModelR² (in-sample)CV RMSEPredictionSizing
119,9995Linear regression (3 features)0.984.87273.88Big
21,49915Linear regression (all)0.969.70220.88Big
3294Linear regression (3 features)0.9323.83 (LOO)268.80Moderate
49,99912Gradient boosting (|r| > 0.05 features)0.2724.68238.90Minimum
579920Ridge, α = 1000.2028.09249.68Minimum
61198Mean baseline0.0054.92172.24Minimum
719,99925Ridge, α = 1000.0415.55214.70Minimum
81,99925Ridge, α = 1000.0426.04202.01Minimum
95920Mean baseline0.0043.74218.60Minimum

R² is the training-set fit reported by strategy.py; it is honest for stocks 1 and 2 (thousands of rows, few parameters) and optimistic for stock 3. RMSE is five-fold CV, except stock 3 where the round notebook used leave-one-out because five folds of six rows are noisy. Re-running strategy.py today reproduces every row; five-fold on stock 3 gives 26.24.

Stocks 1 and 2 — simple models were enough

Both contained strong linear structure: R² above 0.96 with only a small subset of features carrying the relationship. Residual plots looked like irreducible noise rather than missed curvature, and the notebooks bear that out: on stock 1 a search over polynomial, piecewise and transformed features matched the linear fit to the fourth decimal (R² 0.98448 against 0.98447), and on stock 2 ridge and lasso offered nothing over least squares. These were the only rounds where we took our largest positions.

Stock 3 — 29 observations and one influential point

A tiny dataset with a surprisingly strong linear signal. Leave-one-out RMSE was 23.83 for the plain linear model against 30.69 for a degree-2 polynomial and 34.80 for ridge on polynomial features, so the extra flexibility clearly hurt. Cook's distance flagged one highly influential observation: removing it would have shifted the prediction by roughly 10 points and improved leave-one-out RMSE to 18.59. We kept it. With 29 rows and no evidence the point was erroneous, deleting an inconvenient observation would have made the model look better and be more fragile. The signal justified a bet; the sample size capped it at the moderate tier.

Stock 4 — real but weak non-linearity

The one round where extra model complexity helped: gradient boosting on the features with non-trivial target correlation improved R² from about 0.14 to 0.27. That was enough to change the model choice and not nearly enough to change the sizing tier. The model improved; our confidence did not suddenly become high.

Stocks 6 and 9 — sometimes there is no model

We tried linear, ridge and lasso regression, gradient boosting, random forests, k-nearest neighbours, support-vector regression, small neural networks, PCA, polynomial features and mutual-information screening. Nothing consistently beat the mean of the training target under cross-validation. The correct decision was not a more exotic algorithm; it was to accept the uncertainty, predict the mean, and trade the minimum.

Stock 7 — 20,000 rows, almost no information

Roughly 20,000 rows and 25 features, and the best cross-validated model still explained about 4% of variance. Sample size is not evidence of predictability. A large table of uninformative features is still uninformative.

From prediction to position

Two sizing tools existed. The pre-round engine assigned a tier from fit quality and sample size; the live tool, built during the finals, turned the market maker's revealed quotes into probabilities and a Kelly fraction.

Tiered sizing (mine, strategy.py)

if r2 > 0.90 and n_rows >= 500:          # Tier 1: strong fit AND enough data
    shares = int(0.15 * cash / (2 * rmse))  # risk ~15% of cash on a 2·RMSE miss
elif r2 > 0.50 or (r2 > 0.90 and n_rows < 500):
    shares = int(0.05 * cash / (2 * rmse))  # Tier 2: ~5% of cash
else:
    shares = MIN_SHARES                     # Tier 3: 10 shares, no more

shares = max(MIN_SHARES, shares)
shares = min(shares, int(0.25 * cash / max(prediction, 1)))  # never >25% of cash in one trade

Two details matter. A strong fit on a small sample (stock 3) is explicitly demoted to the moderate tier, and a hard cap on capital per trade sits above every tier — the sizing rule cannot produce a position that could eliminate us in one round.

Kelly-based live sizing (Kieran's implementation, used by both of us)

Once the market maker's bid and ask were visible, the live tool estimated the probability that the true value lay below the bid, above the ask, or inside the spread. The residual distribution was modelled as Student's t (degrees of freedom n − 1) when the dataset had fewer than 100 rows and as normal otherwise, with the RMSE inflated for excess kurtosis and the centre shifted for residual skew. The estimated edge fed fractional Kelly sizing at three profiles — half-Kelly by default — with the multiplier halved again when the residuals were fat-tailed.

Survival constraints applied regardless of the model: minimum positions on low-confidence rounds, a cap on capital per trade, smaller positions as the bankroll fell, and no aggressive bets when a strong fit rested on too few rows.

Why it worked

  • Model selection by evidence. Linear models where they were sufficient; boosting only where it demonstrably helped.
  • Model rejection. Six of nine rounds were traded at minimum size because no model earned more — two of them on a plain mean, four on weak ridge or boosting fits.
  • Game mechanics. Never providing liquidity to the field; trading against the winning quote with information the quote-setter did not have.
  • Sizing. Capital concentrated where the CV error was small relative to the edge, with hard caps everywhere else. The 1st-place Sortino ratio is the downside-control half of that story.

Limitations

What this result does and does not show

The bankroll is a simulated competition figure with no transaction costs, no market impact and one trade per round. It says nothing about live trading.

Each round produced exactly one realised outcome, so there is no way to measure whether our probability estimates were calibrated. The Sortino ranking is computed by the organisers over nine round returns — a very small sample.

The R² column is in-sample. For stock 3 in particular the honest number is the leave-one-out RMSE, not the 0.93 fit.

The uncertainty adjustments in the live tool were heuristics, not fitted quantities: a shift of 0.1 × skew × RMSE and a 20% RMSE inflation per unit of excess kurtosis above 3. "Worst case = 2 × RMSE" in the tiered rule is a rough proxy for a tail, not a tail estimate.

The two sizing tools were never compared against each other or against alternatives in a simulator, so "the sizing policy was right" is supported by one competition, not by a distribution of outcomes.

What I would do next

  • Separate reusable modelling code from the round notebooks and put the model-selection rule under tests.
  • Report cross-validated R² alongside RMSE, and bootstrap the small-sample rounds to put intervals on both.
  • Build a round simulator with the competition mechanics and compare tiered, half-Kelly and full-Kelly policies across thousands of synthetic rounds, including the probability of elimination.
  • Replace the skew and kurtosis heuristics with a fitted residual distribution or conformal intervals.

Code

The repository contains the nine training tables, one notebook per round, and three scripts: strategy.py (models, tiers and a live cheat-sheet for each round), kelly_based_strategy.py (interactive live sizing from the revealed quotes) and market_maker_prices.py (scenario planning for possible quotes). python strategy.py reproduces the table above from the data in the repo.