Double Machine Learning: Estimating a Causal Parameter Instead of Predicting Returns
Every modelling article on this blog so far poses the same shape of question: given features, predict a number, then validate that the prediction survives out of sample. Spread models predict spread. Fill models predict fill probability. The whole validation apparatus — purged walk-forward, deflated Sharpe, look-ahead taxonomy — exists to check whether a prediction is real.
This article poses a different shape of question, and it is the one piece of machinery the blog has never had: estimate a single scalar parameter that has a causal interpretation, and attach a standard error to it that survives the fact that a flexible ML model was used to get there.
That is not a rhetorical distinction. "Queue position predicts fill probability" is trivially true and operationally useless — of course it does, both are driven by depth and volatility. "Moving one position forward in the queue causes a change of in fill probability, holding market state fixed" is a number you can put into an order placement policy. The first is a regression fit. The second requires an estimator that does not exist in the standard ML toolbox, because regularization and overfitting in a flexible first stage bias exactly the coefficient you care about.
Double Machine Learning (Chernozhukov, Chetverikov, Demirer, Duflo, Hansen, Newey & Robins, 2018) is the estimator that fixes this. Every quantitative trader has heard it: correlation is not causation. DML is the part that comes after that sentence.
Why Naive Regression Fails
Set up the question in the blog's own territory. We want the causal effect of queue position on fill probability:
- : whether the resting limit order at observation filled within the horizon.
- : queue position at placement (normalized by level size).
- : the confounders — realized volatility, quoted spread, depth imbalance, level size, time of day, regime label. These are the market-state variables the blog already computes in spread modeling with machine learning.
The causal parameter is in
where captures the (potentially complex, nonlinear) relationship between market state and fill outcome.
The treatment is not randomly assigned. You are near the front of the queue because the level was thin, or because you posted during a calm stretch, or because the book was imbalanced in your favour. The same conditions independently drive whether you fill. That is the confounding.
Approach 1: Ignore confounders. Regress on alone. The estimate absorbs the effect of every confounder correlated with both. Textbook omitted variable bias: thin levels give you both a good queue slot and a high fill rate, so you overstate the value of the slot itself.
Approach 2: Linear regression with controls. Regress on and . This works only if is truly linear. Book dynamics are not — the fill/volatility relationship has thresholds, the depth-imbalance effect flips sign by regime. Mis-specifying reintroduces bias.
Approach 3: ML prediction. Fit a gradient boosted model on . You get good out-of-sample discrimination, and no causal interpretation whatsoever. The model captures every predictive pattern, causal or not; regularization shrinks the treatment's contribution in ways that bias ; and there is no standard error you can trust.
This is the core tension. ML is good at prediction, but naive application to a causal parameter produces biased, non-normal, unreliable estimates.
The Partially Linear Model
DML operates within a structural framework. The workhorse is the partially linear regression (PLR):
- is the causal parameter of interest.
- is a nuisance function — the part of the outcome explained by market state.
- is another nuisance function — the conditional expectation of treatment given market state (the "propensity" in a continuous-treatment setting).
- and are structural residuals.
The key insight: is low-dimensional, but and can be arbitrarily complex. We want ML to handle the nuisance functions while still delivering valid inference on the scalar.
Why "Double"?
Two ML models, not one:
- Outcome model: — predict the outcome from market state alone.
- Treatment model: — predict the treatment from market state alone.
Form residuals
and estimate by regressing on :
This is Frisch-Waugh-Lovell on steroids: partial out the confounders with ML instead of a linear projection, then read the treatment effect off the residual variation.
Neyman Orthogonality: Why It Works
The naive partial-out approach (estimate , subtract, regress) fails because ML estimation errors in propagate straight into . The DML score is constructed to be Neyman orthogonal — insensitive to small perturbations in the nuisance functions.
The orthogonal score for PLR:
where . The orthogonality condition is
Intuitively, the score uses only the variation in and that is independent of , and errors in one nuisance function are offset by the other. If slightly over-predicts the treatment, is slightly too small, but the corresponding error in from mis-estimating pushes in a compensating direction. The bias becomes second order — the product of two first-stage errors — rather than first order.
Formally, if both nuisance estimators converge at rate (mild; most reasonable ML methods clear it), then
so converges at the parametric rate and is asymptotically normal.
Cross-Fitting: Why It Is Mandatory Here
Orthogonality alone is not enough. If the nuisance models are fit on the same rows used to estimate , first-stage overfitting contaminates the second stage — and the specific harm is worth stating precisely, because it is not the harm you are used to. Elsewhere, overfitting shows up as an inflated validation score: you notice it, you discount it, you move on. Here it shows up as a shifted point estimate for , with a confidence interval that is still narrow and still centred on the wrong number. There is no score to be suspicious of. The estimator simply lies quietly.
Cross-fitting breaks the dependence: each observation's nuisance predictions come from a model trained without it, and is estimated from the pooled held-out residuals. The mechanics are ordinary K-fold machinery, covered in spread modeling with machine learning; what matters below is which folds you hand it.
The DML Algorithm Step by Step
Input: data , ML methods and , folds .
Step 1 — Partition: split into disjoint folds.
Step 2 — Cross-fit nuisance models: for , train and on the complement of fold , then compute and for .
Step 3 — Estimate:
Step 4 — Inference:
with intervals .
The confidence interval is valid for exactly one question
This is the caveat that decides whether a DML result is worth anything, and it is where most applied uses of the method quietly fall apart.
The asymptotic normality above is a statement about one pre-specified treatment, one pre-specified confounder set, one pre-specified score. Fix those in advance, run the estimator once, and the interval means what it says. Try three candidate treatments, or four confounder sets, or swap learners until the p-value looks better, and you are no longer doing inference — you are running a search, and the reported p-value is the p-value of a maximum, not of a draw.
The blog has already measured what that does. In the deflated Sharpe ratio study, searches over pure noise with zero true edge produce a naive false-discovery rate of 1.000 — the unadjusted test fires every single time — while the median naive p-value of the winner sits near 0.0007. Nothing about Neyman orthogonality protects you from this. Orthogonality fixes the bias from nuisance estimation; it says nothing about the bias from specification search. A DML p-value of 1e-05 obtained after trying six specifications deserves exactly the same Bonferroni/Holm/BHY treatment as any other winner pulled out of a grid, with set to the number of specifications you actually ran.
This has a direct practical consequence for the convenience features of the tooling. DoubleMLData accepts a list in d_cols and will happily return three treatment effects in one summary table:
dml_data_multi = dml.DoubleMLData(
df, y_col='filled', d_cols=['queue_pos', 'toxicity', 'spread_at_post'],
x_cols=confounder_cols,
)
Three rows, three p-values, and a multiple-testing problem the summary table does not mention. If you read all three, adjust all three. If only one was the pre-registered question, say so, and treat the other two explicitly as exploratory.
Cross-Fitting on Time Series: Purge, Embargo, Custom Folds
Standard DML assumes i.i.d. observations. Book data is not, and the failure mode is the one this blog has already documented in detail: adjacent rows share overlapping forward windows, so a plain TimeSeriesSplit still leaks the answer across the fold boundary. See spread modeling with machine learning for the purged, embargoed walk-forward implementation and the reason a gap of at least horizon rows is required, and the look-ahead bias taxonomy for the full catalogue of leaks and their measured magnitudes.
The genuinely DML-specific part is how you hand purged folds to the estimator, because set_sample_splitting has a contract that trips people up:
import numpy as np
import doubleml as dml
def purged_folds(n: int, n_splits: int, horizon: int):
"""Expanding-window folds with a purge/embargo gap of `horizon` rows.
Same construction as the purged walk-forward CV in the spread-modeling
article: the gap removes the overlap between a training row's forward
window and a validation row's.
"""
fold_size = n // (n_splits + 1)
for k in range(1, n_splits + 1):
train_end = fold_size * k
val_start = train_end + horizon
val_end = val_start + fold_size
if val_end > n:
break
yield np.arange(0, train_end - horizon), np.arange(val_start, val_end)
folds = list(purged_folds(len(df), n_splits=5, horizon=HORIZON))
dml_plr = dml.DoubleMLPLR(dml_data, ml_l=ml_l, ml_m=ml_m)
dml_plr.set_sample_splitting([folds])
dml_plr.fit()
Two things to be aware of, neither of which is obvious from the library docs:
- Purged walk-forward folds do not cover every row. The purge gaps and the initial training block are never anyone's test fold, so is estimated from strictly fewer than residuals. That is correct behaviour, not a bug, but it means the effective in the variance formula is the number of pooled test rows — check it rather than assuming.
n_reprepetitions are not free here. With random K-fold, repeating cross-fitting and averaging is a cheap variance reduction. With a deterministic time-ordered split there is only one splitting, son_repbuys nothing and hides nothing; stability has to come from re-running on different data windows instead.
For panel structures (many symbols over the same period), DoubleML supports cluster-robust standard errors — cluster on symbol, not on time, and see multi-symbol validation for the blog's position on when a cross-instrument result is actually established.
The Measured Case: Queue Position and Fill Probability
This is the one causal question in the article where the project already has the data, and it should be run rather than posed. Queue position analysis already covers position estimation, FIFO mechanics, drain rates and time-to-fill on real book data; fill simulation covers fill probability modelling and the calibration loop against live fills. Both produce a predictive model of fills. DML turns the same inputs into a causal estimate.
The specification, pre-registered before looking at the estimate:
- Outcome : filled within
HORIZONsnapshots (binary). - Treatment : normalized queue position at placement.
- Confounders : realized 1s volatility, quoted spread in bps, depth imbalance, level size at post, distance from mid in ticks, time-of-day encoding, regime label.
from sklearn.ensemble import GradientBoostingRegressor
import doubleml as dml
confounder_cols = [
'rv_1s', 'spread_bps', 'depth_imbalance', 'level_size',
'dist_from_mid_ticks', 'tod_sin', 'tod_cos', 'regime',
]
dml_data = dml.DoubleMLData(
df, y_col='filled', d_cols='queue_pos_norm', x_cols=confounder_cols,
)
ml_l = GradientBoostingRegressor(n_estimators=300, max_depth=5)
ml_m = GradientBoostingRegressor(n_estimators=300, max_depth=5)
dml_plr = dml.DoubleMLPLR(dml_data, ml_l=ml_l, ml_m=ml_m, score='partialling out')
dml_plr.set_sample_splitting([folds])
dml_plr.fit()
print(dml_plr.summary)
dml_plr.sensitivity_analysis()
print(dml_plr.sensitivity_summary)
The result to report is a four-column comparison, not a single coefficient: the naive OLS estimate of on alone, the linear-control estimate of on and , the DML estimate with its standard error, and the robustness value from the sensitivity analysis — how strong an unobserved confounder would have to be to nullify the effect. The gap between the naive and DML columns is the quantity of real interest: it is how much of the apparent value of queue position was market state wearing a costume.
A null or negative here is a publishable result and fits this blog better than a clean positive one. If the causal effect of queue position collapses once volatility and level size are partialled out, that is a direct finding about order placement policy: the slot is not what is earning the fill, the conditions under which you got the slot are.
Causal Factor Analysis
The standard approach to factor investing is associational: sort by a characteristic, form long-short portfolios, observe that returns differ. DML enables a different test — estimate the direct causal effect of a characteristic on returns, netting out the confounding effects of the other characteristics. If the effect vanishes under DML, the factor is not independently causal; it is a proxy.
This is a second, independent form of factor skepticism, and it is worth being explicit about how it relates to the one the blog already publishes. The deflated Sharpe ratio attacks the factor zoo from the selection side: with enough trials, a factor can look significant purely because you looked many times. DML attacks it from the confounding side: a factor can look significant on a single honest test and still be a proxy for something else in the conditioning set. A factor has to survive both to be interesting, and the two failure modes are independent — passing one tells you nothing about the other.
What DML Cannot Do
-
It requires the confounders to be observed. If an unobserved variable drives both treatment and outcome, DML is biased, and no amount of ML sophistication fixes an identification problem. Sensitivity analysis bounds the risk; it does not remove it.
-
It estimates an average effect. If the effect of queue position varies sharply across regimes, the point estimate is the average over your sample's regime mix. For heterogeneity use the Interactive Regression Model (
DoubleMLIRM) or a causal forest. -
It assumes a structural model. The partially linear specification requires the treatment to enter the outcome equation in a particular way. If the true process is fundamentally different, DML is confidently wrong.
-
It does not discover causal structure. DML estimates the effect of a pre-specified treatment. It does not tell you which variables are causes.
-
It does not exempt you from multiple testing. Repeating the point above because it is the one most often skipped: orthogonality debiases nuisance estimation, not specification search.
Practical Notes
Sample size. DML needs the nuisance models to converge at , which in practice means enough rows for the ML models to approximate and at all. Rather than trust round-number thresholds, establish adequacy empirically the way multi-symbol validation does — check whether the estimate holds up across instruments and sub-periods, and treat instability as the signal it is.
Learner choice. The DML-specific fact is narrow but useful: given convergence, the learner affects the efficiency of (interval width), not its consistency. Which learners are worth reaching for on tabular market data, and why gradient boosting is the default, is already covered in spread modeling with machine learning. If moves materially across learners, that is not a menu to choose from — it is evidence the nuisance functions are badly estimated, and per the section above, picking the friendliest one turns the exercise into a search.
Conclusion
DML gives the blog something it did not have: a way to state a market-microstructure claim as a causal parameter with a defensible standard error, instead of as a prediction with a good validation score.
The three load-bearing ideas are:
- Orthogonalize the score so first-stage errors cancel to second order.
- Cross-fit, with purged and embargoed folds on time-series data, so first-stage overfitting cannot shift .
- Pre-specify, so the interval you report is the interval you actually earned.
The estimator is the easy part. The hard parts are unchanged: deciding which confounders matter, arguing that the identification assumptions hold, and resisting the urge to run the specification one more time.
References
DML's lineage is short and worth one line: it is Robinson's (1988) partially linear model with ML replacing the kernel estimators, achieving the semiparametric efficiency bound, with an orthogonality condition tracing to Neyman's C() test and a close cousin in the targeted-learning (TMLE) literature. Chernozhukov et al.'s contribution was showing this could be operationalized with arbitrary ML learners while retaining -consistent, asymptotically normal inference.
- Chernozhukov, V., Chetverikov, D., Demirer, M., Duflo, E., Hansen, C., Newey, W., & Robins, J. (2018). Double/Debiased Machine Learning for Treatment and Structural Parameters. The Econometrics Journal, 21(1), C1-C68.
- Robinson, P. M. (1988). Root-N-Consistent Semiparametric Regression. Econometrica, 56(4), 931-954.
- Bach, P., Chernozhukov, V., Kurz, M. S., & Spindler, M. (2022). DoubleML — An Object-Oriented Implementation of Double Machine Learning in Python. Journal of Machine Learning Research, 23(53), 1-6.
- Facure, M. (2022). Causal Inference for the Brave and True. Chapter 22: Debiased/Orthogonal Machine Learning.
- Cahan, E., Bai, J., & Ng, S. (2024). Causal Factor Investing. Quantitative Finance.
Penulis
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.