Machine Learning Signals & Model Ops
A model is just another signal — a number your bot still has to size, risk-manage and route.
This week you learn to earn that number honestly: features known at time t, splits that respect time,
and a validated artifact that predicts inside your per-tick budget.
- Build a feature matrix from prices and the order book without leaking the future into a single column.
- Validate walk-forward and recognise the exact moment a backtest score stops being a measurement.
- Operate a model: persist it, load it once, predict in
on_tick, and watch it drift.
Supervised learning, pointed at the future
Learn a mapping f(X) → y from historical examples. In trading, X is
everything you know now; y is the piece of the future you are predicting.
Every row of your dataset is one point in time. The features in that row must be computable from data that had
already printed at that instant — the last mid, the rolling volatility over the previous twenty ticks, the
current book imbalance. The label belongs to the future: the return from t to t+h, or
merely its sign.
Two label choices, two different jobs. Classification predicts direction and is robust — you can
size by the predicted probability. Regression predicts magnitude, is more informative, and is noisier and
harder. Either way the horizon h must match how long you actually intend to hold: a label about the
next 30 seconds cannot justify a position you keep for an hour.
The golden rule has one clause. Every feature in X must be knowable at t.
Anything else is leakage, and leakage does not look like a bug — it looks like a great model.
# one row per point-in-time observation X = features known AT time t # no future data! y = label about t+h # the horizon # classification -> sign of the next return y = 1 if ret(t, t+h) > 0 else 0 # regression -> magnitude of the next return y = ret(t, t+h) model.fit(X_train, y_train) y_hat = model.predict(X_now) # -> a signal
Splitting in time is the whole discipline
Fitting the model is one line. Everything that decides whether the number means anything happens in how you cut the data — and in how many times you look.
Random shuffling is the default in every tutorial and it is catastrophic on a time series. Shuffle, and a row from next Tuesday sits in your training set while its neighbour — nearly the same market state — sits in the test set. The model does not learn structure; it learns to recognise its neighbours. Your test score becomes a measurement of how similar adjacent ticks are, which is close to 100% and completely useless.
Once the wall is in the right place, one split is still not an evaluation — it is a single sample. Walk-forward (rolling-origin) validation trains on the past, tests on the next unseen block, rolls forward and repeats. Every test block is out-of-sample, which is exactly the situation your bot is in at every tick of the live session: it only ever knows the past.
Accuracy is a vanity metric. Net, risk-adjusted P&L is the score.
A 55% classifier that flips every tick pays the spread endlessly and finishes the season below idle cash. A 52% classifier that trades forty times pays for itself.
The arena settles this argument with numbers. Every fill pays a taker fee or earns a maker rebate, every crossing of the spread costs half of it, and the season leaderboard ranks risk-adjusted return — not raw P&L. So the question a model has to answer is never "is it accurate?" but "does its edge per trade survive the cost of the trades it wants to make?"
Five ways to produce a beautiful, worthless model
Every one of these has shipped in a real fund. Each expands.
train_test_split(shuffle=True) on a time series
X[:cut] / X[cut:] —
and purge the h rows that straddle the boundary.A feature that quietly contains its own label
on_tick — one function, two callers.Tuning on the test block (data snooping)
Feature mismatch between training and on_tick
A model you cannot run inside a tick
on_tick. Your bot misses the market it correctly predicted, and the season's rate limits (enforced from Week 8) throttle the
backlog. The tell: your fills print at prices two ticks behind your signals.
__init__, time predict() against the per-tick budget
from W6, and prefer a small model you can actually run. A model you can't run live scores nothing.Three questions
Instant feedback, no grading. Pick the answer you would defend in a demo.
h = 10 ticks and split chronologically at 70%. Why is an
embargo still needed?h of the cut straddle it. Purge those h rows and the boundary is genuinely sealed —
the cost is a handful of samples.exchange/scoring.py ranks return per unit of volatility with a
drawdown penalty, so the churn is punished twice.This week's work
Deliverable: a model integration plan plus the walk-forward evaluation checklist — features,
label, horizon, folds, one test look, measured predict() latency.