
In June 2019 I stop treating feature scaling as “preprocessing” and start treating it as part of the production contract - same transforms, same stats, same order — or the live system lies.
Axel Domingues
In 2018, Reinforcement Learning (RL) trained me to distrust “it seems to work.”
In 2019, trading is doing the same thing — but with money-shaped consequences.
This month is where I learn a painful lesson: normalization isn’t a data science detail.
It’s a deployment problem.
If the model sees one distribution in training and a different distribution in production — even because I recomputed mean and sigma (standard deviation) the “easy way” — I’m not shipping intelligence. I’m shipping drift.
I thought the problem was:
“My features have different scales — I should normalize.”
The real problem was:
“My training and live pipelines must apply the same transform, with the same parameters, in the same feature order, forever.”
Because in live trading I can’t “just fit a scaler.”
In that world, a scaler is a contract, not a convenience.
It won’t explode immediately — it will just look brilliant in backtests.
By now (after March + April), my collector is producing snapshots and writing them through the output pipeline. The model-facing table includes things like:
If you stare at those raw columns for 5 minutes you notice:
So I decide to treat normalization as first-class infrastructure: generated offline, versioned with the model, loaded in the live bot.
That’s what Util/produceMeanSigma.py does.
A few details that matter a lot:
mean.npy and sigma.npy — and those become part of the model version.Here’s the “shape” of the pipeline in plain English:
data tablex_features_colsmean + sigma per columnmean.npy and sigma.npy:A second issue shows up quickly: even if you normalize features, the price level itself moves over months.
Absolute price-based signals are fragile, because “10 dollars” means different things at different BTC levels.
So I introduce a relative price feature: mid price divided by the BitMEX index (the “indicative settle” reference).
That’s what Util/produceBitmexIndexDiff.py produces:
idx_diffvalid = 1 (the schema starts to care about validity explicitly)In other words:
Don’t let the model learn “BTC at 3k behaves like this and BTC at 10k behaves like that”
when what I really want is “price relative to fair reference is stretched or compressed.”
This also makes later walk-forward evaluation less brittle, because the feature space is closer to “stationary” (still not stationary — just less ridiculous).
It’s the same engineering instinct as normalizing sensor readings in robotics:
you want the model to learn relationships, not raw units.
The most important place normalization appears is not training.
It’s the bot.
In BitmexPythonChappie/OrderBookMovePredictor.py, I load the saved arrays and apply the transform exactly once per prediction:
mean-truncated.npy and sigma-truncated.npyFEATURES_COLS from the current snapshot(x - mean) / sigmainf, -inf, and NaN with 0That last part is not “nice to have.” It’s how you stop live inference from crashing — or worse, outputting garbage while looking healthy.
The contract becomes:
Anything else is a silent fork.
That’s why I treat
FEATURES_COLS,mean, andsigmaas one versioned unit.
This is the checklist I keep in the repo now — because I don’t trust my future self:
saved_networks/<model>/mean-*.npysigma-*.npyIf a feature becomes constant in the dataset:
First fix: drop the feature or clamp sigma with a small floor — but only intentionally.
If I allow “whatever NumPy does” to leak into decisions, I deserve the bug.
At minimum:
idx_diff in the datasetRun Util/produceBitmexIndexDiff.py on the HDF5 file(s) you intend to treat as “truth” for training.
What I verify:
idx_diff column existsmean.npy and sigma.npyRun Util/produceMeanSigma.py over the training dataset directory.
What I verify:
Copy the produced arrays into the model directory (later under saved_networks/).
What I verify:
In BitmexPythonChappie/OrderBookMovePredictor.py, validate:
If normalization is a deployment problem, I need deployment-grade telemetry.
This month I start logging/plotting:
mean, sigma summary tableIn 2016 I learned “feature scaling improves optimization.”
In 2017 I learned “training stability is engineering.”
In 2018 I learned “RL breaks if your signal is inconsistent.”
In 2019 I add a sharper rule:
If preprocessing can’t be reproduced byte-for-byte in production, it isn’t preprocessing — it’s a bug factory.
My research repo
The full research repo where these normalization artifacts are generated and then loaded by the live bot.
Mean/Sigma generator
Offline stats generation: compute and save mean.npy + sigma.npy as versioned artifacts.
Because it changes the meaning of your inputs over time. The model was trained on “this feature value means X relative to fixed stats.” If you re-fit the scaler in production, you silently move the goalposts.
If I want adaptive normalization, it must be part of the modeling design — not an accidental side effect.
It can be — which is why I log the NaN rate aggressively.
The goal isn’t to pretend missing data doesn’t exist. The goal is:
It removes one obvious source of non-stationarity: the absolute BTC price level.
It doesn’t solve regime shifts, but it stops the model from learning a brittle “price-era signature” that disappears when the market moves into a new range.
Compare a single snapshot processed in:
If the normalized vectors aren’t identical (or extremely close), I treat it as a release blocker.
Next month I stop hiding behind “features” and define alpha — the label that claims “this is the move that matters.”
And I’m already nervous, because the easiest way to cheat in trading is not in the model.
It’s in the target.
Defining Alpha Without Cheating - Look-Ahead Labels and Leakage Traps
Before I train anything, I need a label that doesn’t smuggle the future into my dataset.
Feature Engineering, But Make It Microstructure: Liquidity Created/Removed
If the order book is the battlefield, features are the sensors. This month I stop hand-waving and teach my pipeline to measure liquidity being added and removed - in a way I can deploy live.