Replicating Vilkov's 0DTE trading rules
His code on his 953-session panel, then his rules on 46 sessions of our own SPXW tape
Most published 0DTE results cannot be checked: the sample is proprietary, the code is described rather than shipped, and the reader is left comparing a number to a memory. This one can. Vilkov released the package — MIT licence, the panels his tables are built from, the scripts that build them — so the paper is a rare thing in this literature: an falsifiable object. This post does two things with it. First, run his construction against his own shipped data and see whether his tables come back. Second, run the same rules against a tape we built independently and see whether the result survives. The two questions have very different answers, and the second one is mostly a statement about sample size. There is also a third part, which was not planned: we sent the cost finding to the author before publishing, and his reply — confirming it, rerunning the private sample behind the paper, and pushing a fix upstream — is §3.
What the paper claims, stated so you could trade it
The instrument is SPXW, European, cash-settled at the 16:00 ET index close. Entry is a single fixed intraday bar-end — 10:00 ET in the main text, with 13:00 and 15:00 in the appendix. There is no exit: the position is held to the 16:00 cash settlement, with no stop, no take-profit and no intraday management. Sizing is one unit of each structure per day, unlevered, and P&L is reported per unit of underlying. No hedge is applied anywhere.
He does not trade listed strikes. Per bar and per option type he keeps same-day-expiry
quotes with moneyness M = K/S in [0.97, 1.03], divides mid price and bid-ask
spread by spot, and Akima-interpolates mid, spread, IV and the greeks onto the grid
M ∈ {0.980, 0.981, …, 1.020}. So a "0.995 put" is a synthetic contract at
exactly 0.5% out of the money, not a listed strike. Seven structures are built from those
legs, across 47 structure × moneyness configurations:
| structure | legs (lo/hi = lowest/highest moneyness in the tuple) | what it is |
|---|---|---|
| Strangle/Straddle | +1 P(lo), +1 C(hi) | long volatility |
| Iron Butterfly/Condor | +1 P(lo), −1 P(mlo), −1 C(mhi), +1 C(hi) | net credit, short volatility |
| Risk Reversal | −1 P(lo), +1 C(hi) | long delta |
| Bull Call Spread | +1 C(lo), −1 C(hi) | long delta |
| Bear Put Spread | −1 P(lo), +1 P(hi) | short delta |
| Call Ratio Spread | +1 C(lo), −2 C(hi) | short upside tail |
| Put Ratio Spread | −2 P(lo), +1 P(hi) | short downside tail |
The sign convention matters and is easy to misread: his "Strangle/Straddle" is the long strangle, so his headline SR of −0.51 is a long-volatility position losing money, not a short straddle. His "Iron Condor" is the credit condor, so its −0.96 is short volatility. Both being negative is the paper's central point — long vol loses to the premium, short vol loses to the spread and the tails.
Costs are charged in three tiers: mid, mid minus half the quoted spread on every leg,
and mid minus half-spread minus a flat 0.5 bp of underlying, charged once per structure per
day regardless of direction. Sharpe is daily mean over daily sd, times √252. On top of the
unconditional table sits a conditional layer: an L2 logistic classifier on implied
variance and semivariances, implied skew, surface slopes, lagged realised moments, lagged
structure P&L, leg-level liquidity, flow and OI-weighted gamma aggregates, fitted with a
252-day burn-in and a strictly out-of-sample window from April 2019. The target is
binary — 1[PNL_net > 0] — and he is explicit that
classifying the sign beats predicting the magnitude. The position is
w = sign(p̂ − 0.5).
What we did
Two runs, and it is worth keeping them apart.
Run one, his data. We pulled his package at commit
962ae9b (2026-06-16), MIT licensed, including the LFS data panels behind his
published tables, and re-implemented his construction from his own scripts rather than
calling them. Then we fed his shipped panels through our implementation and compared cell
by cell. Note that the shipped panel is not the paper's stated sample: the paper says
09/2016–01/2026, while the panel and every shipped table stop at 2024-05-01, 953
sessions. Every "his" number below is the shipped one, because that is the one
that can be audited — and the shipped tables reproduce the numbers quoted in the paper's
text exactly. His option source is Cboe 30-minute bars with NBBO quotes and sizes.
Run two, our data. The same seven structures and 47 configurations built from our own SPXW tick tape, on 46 sessions, 2026-05-01 to 2026-07-31 — a window entirely outside his sample, so genuinely out of sample, and very short. Built two ways: interp, his construction (Akima over the moneyness grid, P&L = payoff − mid, cost = half the quoted spread per leg + 0.5 bp), and strike, tradable (nearest listed strike, buy at the ask, sell at the bid, $1.25 per contract per leg, settled on the official SPX close). The two agree closely — position deltas match to <0.006 and mean P&L to <0.004% of spot — because 5-point SPX strikes are 0.07% of a 7,300 index. Interpolation is not doing any work on today's strike grid.
1. His package against itself: exact
Feeding his shipped panels through our implementation of his construction returns his published implementable-P&L table on all seven structures, in both the mid tier and the net tier, to four decimals.
| structure | n | mean mid, his | ours | SR mid, his | ours | net, his | ours | SR net, his | ours |
|---|---|---|---|---|---|---|---|---|---|
| Strangle/Straddle | 953 | −0.0059 | −0.0059 | −0.27 | −0.27 | −0.0110 | −0.0110 | −0.51 | −0.51 |
| Iron Butterfly/Condor | 953 | −0.0073 | −0.0073 | −0.56 | −0.56 | −0.0125 | −0.0125 | −0.96 | −0.96 |
| Risk Reversal | 953 | +0.0157 | +0.0157 | +0.65 | +0.65 | +0.0106 | +0.0106 | +0.44 | +0.44 |
| Bull Call Spread | 953 | −0.0101 | −0.0101 | −0.35 | −0.35 | −0.0154 | −0.0154 | −0.53 | −0.53 |
| Call Ratio Spread | 953 | −0.0140 | −0.0140 | −0.55 | −0.55 | −0.0194 | −0.0194 | −0.76 | −0.76 |
| Bear Put Spread | 953 | +0.0138 | +0.0138 | +0.48 | +0.48 | +0.0085 | +0.0085 | +0.30 | +0.30 |
| Put Ratio Spread | 953 | +0.0251 | +0.0251 | +1.06 | +1.06 | +0.0198 | +0.0198 | +0.84 | +0.84 |
Means in % of underlying per day, equal-weighted across each structure's moneyness combos. Exact on every cell.
That is the first result, and it is a clean one: there is no ambiguity left about what he did. Every construction choice the paper leaves implicit — where the 0.5 bp is charged, whether a hedge is applied, what the equal-weighting is over — is pinned down by the fact that the numbers come back identical when those choices are made one particular way. Replications usually stall here. This one did not.
2. What the reproduction turned up: a cost-scale error
In the shipped data, the P&L column reth_und is stored as
percent of spot — verified directly: for the first row of the ATM call
panel, payoff − mid = −0.001385 and reth_und = −0.138517, exactly
×100. The spread column bas is stored as a fraction of spot:
its mean is 0.00042, i.e. 0.042% of spot, about $1.89 at S = 4500, which is the correct
order of magnitude for an SPXW 0DTE market. His implementable-P&L script then subtracts
the fraction from the percent. The flat 0.5 bp term is on the right scale — his own in-code
comment says the units are percent — but the half-spread is not. The consequence is that
the model charges 1% of the spread it computes.
It is visible in his own published table without running anything: his "Mean Mid" and "Mean B/A" columns differ by 0.0001, which is to say that crossing the entire SPXW bid-ask on two to four legs is charged at one hundredth of a basis point. Put the half-spread on the same scale as the P&L and change nothing else:
| structure | n | true half-spread | net, as published | SR, as published | net, corrected | SR, corrected |
|---|---|---|---|---|---|---|
| Strangle/Straddle | 953 | 0.0107 | −0.0110 | −0.51 | −0.0215 | −0.97 |
| Iron Butterfly/Condor | 953 | 0.0251 | −0.0125 | −0.96 | −0.0374 | −2.67 |
| Risk Reversal | 953 | 0.0082 | +0.0106 | +0.44 | +0.0024 | +0.10 |
| Bull Call Spread | 953 | 0.0317 | −0.0154 | −0.53 | −0.0468 | −1.59 |
| Call Ratio Spread | 953 | 0.0354 | −0.0194 | −0.76 | −0.0544 | −2.11 |
| Bear Put Spread | 953 | 0.0297 | +0.0085 | +0.30 | −0.0209 | −0.73 |
| Put Ratio Spread | 953 | 0.0343 | +0.0198 | +0.84 | −0.0142 | −0.61 |
Half-spread and net means in % of underlying per day, on his 953 sessions.
Every structure goes negative. The compact version: the mean gross edge on his best structure — the put ratio spread, +0.0251% of spot — is smaller than the spread you cross to get it, 0.0343%.
The same half_spread + 0.005 construction feeds the conditional pipeline,
so the correction propagates to every net number in the paper. Joining his shipped
out-of-sample predictions (682 OOS days in the shipped vintage) to the panel on his
representative near-ATM moneyness:
| structure | protocol | n | long share | SR gross | SR net, as published | SR net, corrected | delta share of gross |
|---|---|---|---|---|---|---|---|
| Put Ratio Spread | expanding | 681 | 44% | +1.81 | +1.55 | +0.33 | +20% |
| Put Ratio Spread | rolling | 681 | 40% | +1.82 | +1.56 | +0.35 | +28% |
| Strangle/Straddle | rolling | 682 | 9% | +0.90 | +0.73 | +0.28 | +5% |
| Strangle/Straddle | expanding | 682 | 6% | +0.39 | +0.22 | −0.23 | +11% |
| Risk Reversal | rolling | 682 | 85% | +0.32 | +0.20 | −0.13 | +149% |
| Risk Reversal | expanding | 682 | 91% | +0.12 | −0.00 | −0.33 | +268% |
| Iron Butterfly/Condor | rolling | 682 | 38% | +1.08 | +0.10 | −3.81 | −2% |
| Bull Call Spread | expanding | 682 | 5% | +0.02 | −0.37 | −1.54 | +1031% |
| Bear Put Spread | expanding | 681 | 5% | −0.07 | −0.45 | −1.67 | +593% |
| Call Ratio Spread | rolling | 682 | 30% | −0.12 | −0.34 | −1.27 | +41% |
And his top-3 basket, the headline portfolio result:
| top-3 basket (put ratio + risk reversal + strangle) | n | SR gross | SR net, as published | SR net, corrected | delta share |
|---|---|---|---|---|---|
| expanding window | 682 | +0.89 | +0.62 | −0.25 | +41% |
| rolling window | 682 | +1.38 | +1.10 | +0.17 | +41% |
The put ratio spread is the only thing left standing, at SR ≈ +0.33 net, +0.66 bp/day, on a signal fitted in-sample-by-protocol over 682 days. That is not nothing. It is also about a fifth of the published number, and it is one structure out of seven with no multiple-testing correction. And — see §3 — it does not survive either: the cost also sits inside the classifier's training label, and the author's retrain with corrected labels takes this same number to −0.70.
The bound on this finding, as originally written. We can audit the shipped code and the shipped data, and nothing else. If the private pipeline that produced the SSRN PDF applied the cost differently, the published PDF numbers could be right and only the public package wrong. That bound no longer binds: the author has since rerun the corrected code on the private vintage behind the PDF, and the bug is present there too — his words in §3.
3. The author's response: confirmed, and made stronger
We sent the finding to Grigory Vilkov on 2026-08-24, before publishing, framed with the one escape route we could not close from outside: if the private pipeline behind the SSRN PDF applied the cost differently, this would be a defect in the public replication package and not in the results. His reply came on 2026-08-26. He is quoted here with his permission.
Thank you for this. You have read the units correctly, the finding stands, and it is more consequential than the charitable version you offered me. Please publish it.
He closed the escape route himself:
I reran the corrected code against the private ver-2026-02-02
vintage behind the PDF, the one with 1,061 out-of-sample days, and the bug is present
there too. Both vintages flow through the same analysis scripts. The private pipeline does
not apply the cost differently. This is a defect in the results, and the correction
reverses the sign of the paper's third headline finding.
He also added a fourth unit check, internal to his codebase and cleaner than any of
ours: his own descriptive script, option_strats_uncond_analysis.py, already
multiplies bas and mid by 100 before reporting them — the
codebase was inconsistent with itself about the scale, so the cost path was wrong rather
than the units being ambiguous. His corrected numbers on the paper's own sample:
| conditional OOS net Sharpe, expanding, paper's sample | published | corrected, author's rerun |
|---|---|---|
| Put Ratio Spread | +0.93 | −0.75 |
| Strangle/Straddle | +0.39 | −0.08 |
| Iron Butterfly/Condor | −0.20 | −3.52 |
| Top-three basket | +0.82 | −0.82 |
| All-strategies basket | +0.25 | −1.60 |
The author's rerun on the private 1,061-OOS-day vintage. No strategy and no basket survives; his unconditional table goes the same way (put ratio +0.49 → −0.85, bear put +0.08 → −0.90). Our corrected magnitudes on the shipped vintage — put ratio +0.84 → −0.61, risk reversal +0.44 → +0.10 — he reproduces to two decimals, and he confirms the shipped counts, 953 sessions and 682 OOS days.
Why his corrected conditional number is −0.70 where ours was +0.33.
Both are right; they measure different things, and the difference is itself informative.
The classifier's target is y = 1[PNL_net > 0], so the cost sits inside the
training label, not only in the P&L accounting. Our +0.33 took his shipped
predictions as fixed and re-priced them at the corrected cost — the pure arithmetic
effect. His −0.70 rebuilds the labels with the corrected cost and retrains, and the model
that comes back is genuinely weaker: fewer days are labelled profitable, and the learned
signal shrinks. On the shipped vintage the full chain is +1.55 published → +0.33
re-priced → −0.70 retrained: most of the published conditional Sharpe was cost
arithmetic, and what the arithmetic left standing was the model having learned from
mislabelled days. The same mechanism moves his gross columns (put ratio gross SR
1.18 → 0.43) even though gross P&L contains no cost term — the predictions changed,
not the prices.
The 1.55-versus-0.93 gap is also resolved, and it is not a bug. The shipped vintage ends 2024-05-01 and was a materially stronger sub-sample, and the 0.93 in the current PDF already carries a different, earlier cost correction — a sign error on short days reported by Victor Yoong in May 2026 as the repository's issue #1. Running the original code on the shipped vintage reproduces the shipped reference table exactly, so the gap is sample period plus that earlier fix, not a public-versus-private difference in cost treatment.
What he did about it. The fix is upstream as commit
85a447c:
the half-spread scaled to percent in all three cost paths, a cost_units.py
guard that makes this class of error fail loudly, a KNOWN-ISSUES.md
documenting both this and the May sign error, and regenerated tables and reference
fixtures. At the time of writing the LFS data panels and the shipped predictions lag the
code (an LFS-budget matter), and the SSRN PDF lags the repository by a few days. The paper
is being revised to frame the conditional section as gross signal value versus net
implementability. His own summary of where that leaves the result:
… the honest reading of the corrected evidence is that 0DTE directional information is real and that the binding constraint is microstructural rather than statistical, which is a more useful finding than the one I originally reported.
That reading is fair, and it survives every correction on this page: the classifier's gross conditional Sharpe stays positive after retraining. The directional information exists. What died is the claim that crossing the quoted spread leaves any of it for the trader — and, per §5 below, the near-ATM spread is about 6× tighter today than his sample average, so the microstructural bar he now names as the binding constraint is itself a moving target.
4. His rules on our tape: 46 sessions decide nothing
Now the second comparison, and the honest answer is that it is not a test. Our window is 46 sessions; the bootstrap 95% interval on an annualised Sharpe over it is 9.8 Sharpe units wide.
| structure | n | gross (mid) | our half-spread | net, tradable | SR net | bootstrap 95% CI | his mean | his SR | sign agrees |
|---|---|---|---|---|---|---|---|---|---|
| Strangle/Straddle | 46 | −0.0128 | 0.0016 | −0.0142 | −2.47 | [−9.53, +1.97] | −0.0110 | −0.51 | yes |
| Iron Butterfly/Condor | 46 | −0.0116 | 0.0040 | −0.0184 | −1.97 | [−6.32, +2.76] | −0.0125 | −0.96 | yes |
| Risk Reversal | 46 | −0.0020 | 0.0012 | −0.0034 | −1.36 | [−4.83, +4.73] | +0.0106 | +0.44 | no |
| Bull Call Spread | 46 | +0.0082 | 0.0164 | −0.0074 | −0.55 | [−5.26, +4.14] | −0.0154 | −0.53 | yes |
| Call Ratio Spread | 46 | +0.0179 | 0.0170 | +0.0018 | −0.18 | [−4.69, +4.60] | −0.0194 | −0.76 | yes |
| Bear Put Spread | 46 | +0.0033 | 0.0188 | −0.0146 | −0.77 | [−6.47, +3.64] | +0.0085 | +0.30 | no |
| Put Ratio Spread | 46 | +0.0110 | 0.0195 | −0.0075 | −0.59 | [−5.86, +3.83] | +0.0198 | +0.84 | no |
Means in % of underlying per day, one observation per session, equal-weighted over his moneyness combos. "Net, tradable" fills at the bid and ask on listed strikes with $1.25 per contract per leg.
His reference Sharpe sits inside our interval for 7 of 7 structures. Read that as "this window cannot test him", not as agreement. Four of seven signs agree and three do not, and none of that is significant. To separate his best published number, SR 0.84, from zero at 5% you would need about 1,372 sessions — roughly 30× what we have.
The gross means have roughly the right shape — his put ratio spread earns +0.0251% of spot gross against our +0.0110%, his bear put +0.0138 against our +0.0033, his strangle −0.0059 against our −0.0128 — but "roughly the right shape" at n = 46 is not evidence of anything. What decides the net column is the cost, which is deterministic, while the P&L is not. Hence every net figure is negative and every Sharpe is uninformative.
In dollars, one 1-lot structure per session at tradable fills:
| structure | n | mean $/session | median $ | t | win% | worst $ | total $ |
|---|---|---|---|---|---|---|---|
| Strangle/Straddle | 46 | −105 | −239 | −0.76 | 35% | −1,477 | −4,810 |
| Iron Butterfly/Condor | 46 | −137 | −80 | −0.76 | 48% | −3,662 | −6,303 |
| Risk Reversal | 46 | −27 | +86 | −0.26 | 83% | −3,208 | −1,237 |
| Bull Call Spread | 46 | −58 | +106 | −0.14 | 52% | −5,467 | −2,687 |
| Call Ratio Spread | 46 | +10 | +164 | +0.02 | 52% | −4,988 | +449 |
| Bear Put Spread | 46 | −105 | −1,203 | −0.23 | 37% | −4,650 | −4,838 |
| Put Ratio Spread | 46 | −53 | −1,046 | −0.13 | 37% | −3,865 | −2,461 |
Not one t-statistic exceeds 0.8 in absolute value. The risk reversal wins 83% of sessions and still loses money — a clean picture of the tail profile these structures carry.
How unstable is a 46-session mean? While this run was in progress the backfill added two sessions, and those two alone moved the tradable put ratio spread from +0.0015% to −0.0075% of spot per day — a step of 0.009%, a third of his entire published gross edge, from two days out of 46. Six of seven structures change sign between the first 60% and the last 40% of the window, or between entry times. That is what n = 46 looks like.
One place where his sign does not survive on our window, reported because hiding it would be dishonest: the single most-quoted structure, the 1/1 straddle (which is long volatility in his convention), is net positive here at tradable fills — +0.0064% of spot at a 10:00 entry (SR +0.31, 48% win rate) and +0.0245% at 13:00 (SR +1.38, 39% win rate), negative at 15:00 at −0.0082%. Positive with a sub-50% win rate is tail-driven, the classic long-vol shape. It is 46 days, the interval covers zero comfortably, and the 13:00 number rests on a handful of afternoon tails. It is not a recommendation to buy 0DTE straddles.
5. Where we agree, where we diverge, and why
Agreement, and it is exact: his tables are his code path. Every cell of the unconditional result reproduces. That is settled.
Divergence one — the cost tier. His net numbers and ours differ because of the scale error above, not because of data. The reason is stated in his own files: the half-spread column is a fraction of spot, the P&L column is percent of spot, and the subtraction mixes them.
Divergence two — three of seven signs on our tape. The document's explanation is sample size and nothing else: the CI width is 9.8 Sharpe units, his figure sits inside our interval on all seven structures, and two backfilled sessions were enough to flip a sign. No further explanation is offered, and none is warranted.
Divergence three — the cost bar itself has moved, and this is the one genuinely new fact. Quoted SPXW spreads on our window are much tighter relative to spot than his 2016–2024 average:
| structure | our half-spread | his half-spread | ratio |
|---|---|---|---|
| Strangle/Straddle | 0.0016 | 0.0107 | 0.15× |
| Iron Butterfly/Condor | 0.0040 | 0.0251 | 0.16× |
| Risk Reversal | 0.0012 | 0.0082 | 0.15× |
| Bull Call Spread | 0.0164 | 0.0317 | 0.52× |
| Call Ratio Spread | 0.0170 | 0.0354 | 0.48× |
| Bear Put Spread | 0.0188 | 0.0297 | 0.63× |
| Put Ratio Spread | 0.0195 | 0.0343 | 0.57× |
Averaged over the seven, spreads are 2.2× tighter relative to spot (0.0112% against 0.0250%). That average is dragged down by the structures carrying a 2%-in-the-money leg, whose markets have barely improved. For the near-ATM combos — all legs within 1% of spot, which is where every one of his conditional results lives — the gap is 6.4× (0.0028% against 0.0178%), and for the ATM straddle alone 6.0× (0.0034% against 0.0204%). Verified independently against raw quotes: the 0DTE ATM straddle at 10:00 ET costs a mean 0.249 index points of half-spread on our window, median 0.175, against 0.733 points in his sample. A consequence worth noting in both directions: his flat 0.5 bp add-on is now 1.8× the actual half-spread on near-ATM structures, so on today's market his cost model is dominated by a made-up constant — which is also the only part of his cost that was on the right scale to begin with.
What is not a divergence: hidden beta. The obvious suspicion about a 0DTE result measured over a bull market is that it is a levered long index position wearing an options costume. On his own 953 sessions, using his own entry deltas, it is not:
| structure | delta at entry | gross | delta term | non-delta | delta share of gross |
|---|---|---|---|---|---|
| Strangle/Straddle | −0.017 | −0.0059 | −0.0015 | −0.0044 | +26% |
| Iron Butterfly/Condor | −0.013 | −0.0073 | −0.0004 | −0.0069 | +5% |
| Risk Reversal | +0.199 | +0.0157 | +0.0024 | +0.0132 | +16% |
| Bull Call Spread | +0.603 | −0.0101 | −0.0003 | −0.0098 | +3% |
| Call Ratio Spread | +0.514 | −0.0140 | −0.0007 | −0.0133 | +5% |
| Bear Put Spread | −0.550 | +0.0138 | +0.0004 | +0.0134 | +3% |
| Put Ratio Spread | −0.431 | +0.0251 | +0.0023 | +0.0228 | +9% |
His two positive structures are 84–91% non-delta, and in his own out-of-sample predictions the put ratio spread's delta term is 20–28% of gross. The reason is mechanical and checkable: over his 953 sessions the mean index return from 10:00 ET to the close is +0.15 bp/day, sd 90 bp, t = +0.05, and the compounded 10:00→close return is −2.4% while the index itself rose +83.4% over the same days. The S&P's drift over that period lived overnight and in the 09:30–10:00 window; his holding period contains essentially none of it. His own caveat — that some favourable outcomes may partly reflect the upward drift of the index — does not apply to his own design. The one structure that is a delta trade in disguise is the risk reversal: its delta term exceeds its entire gross P&L (149–268%), so its non-delta component is negative, and it is long on 85–91% of days. It is also not one of the winners once the spread is charged correctly.
His one conditioning rule that is runnable at n = 46 is a null. Sorting our sessions into terciles of the 10:00 ET integrated 0DTE implied variance (implied move to the close: 0.48%, 0.58%, 0.82% of spot), no structure shows a high-minus-low gap distinguishable from a relabelling of the days — 5,000 permutations, and the probability that the best of seven produces a gap that big by chance never drops below 0.96. His own table reports one significant cell out of seven for the put ratio spread (High−Low = +0.0777%/day, t = 2.50, p = 0.013) with no such correction; at seven tests a Bonferroni threshold would be p < 0.007 and it would not survive. We cannot refute his cell at n = 46. We can only note that ours is nowhere near it, that our sign for the put ratio spread is the opposite of his, and that his was never corrected for the seven looks.
What it does not say
It is no longer an outside inference about the paper. The cost-scale
finding began as one, verified three ways — reth_und is exactly 100 ×
(payoff − mid); bas is at the economically correct magnitude for an SPXW
market; his own in-code comment puts 0.5 bp at 0.005 in the P&L's units — and supported
by a published "Mean Mid" versus "Mean B/A" gap of 0.0001 that no real spread can produce.
It stopped being an inference on 2026-08-26, when the author confirmed the reading, reran
the private pipeline behind the SSRN PDF, and fixed the public package (§3).
It does not test his result on new data. n = 46 is not a sample. Every Sharpe in section 3 is uninformative, and the only statements here with statistical content come from his 953-session panel — his data, not ours. Our window also has a hole (no sessions between 2026-07-01 and 2026-07-24, and 2026-05-19 and 2026-06-03 missing) and covers three calendar months in a single volatility regime, implied move to the close 0.48% to 0.83%. It is not a regime sample.
It does not run his model. His protocol needs a 252-day burn-in; we have 46 sessions. The tercile split above is descriptive, not his logistic.
It is not a comparison against the paper's stated sample. The shipped package stops at 2024-05-01 with 953 sessions and 682 OOS days, against the paper's 01/2026 and 1,061 OOS days. Where the shipped numbers differ from the published ones — put ratio OOS SR net 1.55 shipped against 0.93 published — we used the shipped ones because those are the ones that can be audited. That gap is now fully accounted for (§3): a stronger sub-sample, plus an earlier and unrelated sign-error fix already reflected in the published number.
The delta decomposition is first order. The gamma term is large and offsets against theta, so a three-way split would be unstable; only the two-way delta / non-delta split is load-bearing above.
Nothing on this page is investment advice; see the Terms.
What the replication does settle
Three things, in descending order of confidence. His unconditional table is exactly his code path, and with the half-spread on the right scale all seven structures are negative on his own data — which is a second, independent confirmation that unconditional 0DTE premium selling does not pay. His conditional edge is not a hidden long-index position, so "it is really drift" is not the reason to set it aside; the reason is the cost model. And the near-ATM cost bar has genuinely fallen by about 6× since his sample, which means any result that died at an older spread assumption is worth re-pricing against today's actual quotes — a measured fact rather than an assumption.
When this post was first drafted, the put ratio spread at SR ≈ +0.33 net corrected was the one thing left standing. The author's retrain took that away too: with the cost corrected inside the training labels, nothing in the conditional table survives net of costs, on either vintage (§3). What is genuinely left standing is smaller and better defined — the gross directional signal is real, the binding constraint is the spread, and the near-ATM spread is about 6× tighter today than his sample average. Whether that combination clears zero on today's quotes is a question his corrected package can now be pointed at honestly, and it is exactly the one 46 sessions cannot answer.
Reproduce it
His half is free. The package is public and MIT licensed, and the LFS panels behind his published tables ship with it, so the entire reference side of this post — the exact reproduction, the cost-scale check, the corrected table, the delta decomposition of his sample and his out-of-sample predictions — can be redone with no paid data at all:
# HIS HALF -- free. Public package, MIT licence, data panels included. git clone https://github.com/vilkovgr/0dte-strategies git checkout 962ae9b # the commit audited here, 2026-06-16 git lfs pull # data_structures.parquet, data_opt.parquet, OOS predictions # HEAD now carries the author's fix (commit 85a447c, 2026-08-26): the half-spread # scaled to percent in all three cost paths, a cost_units.py scale guard, and # KNOWN-ISSUES.md. Check out 962ae9b to reproduce the bug, HEAD the correction. # The construction: code/build_data.py::_all_strategy_configs and # compute_implementable_pnl.py::get_legs define the 7 structures and 47 configs. # Sharpe = daily mean / daily sd * sqrt(252). 953 sessions, entries at 10:00 ET. # The unit check, on the shipped panel -- three lines, no model: # first ATM call row: payoff - mid = -0.001385, reth_und = -0.138517 -> reth_und is PERCENT # bas.mean() = 0.00042 (0.042% of spot, about $1.89 at S = 4500) -> bas is a FRACTION # compute_implementable_pnl.py then does, in effect: pnl_ba = reth_und - half_spread_cost # percent minus fraction <-- the error pnl_ba_fee05 = pnl_ba - 0.005 # percent minus percent <-- correct # corrected: pnl_net = reth_und - (100.0 * half_spread_cost) - 0.005 # The same tc = half_spread_cost + 0.005 appears at line 195 of # compute_conditional_oos_protocol.py and is applied at line 315, so every # conditional net number inherits it. # The delta split, valid on any sample size: # PNL = delta_t * (S_T/S_t - 1) + [ everything a delta-hedged position would earn ] # delta share of gross = (delta term) / (gross). Entry deltas ship with his panel.
Our half is not free. The 46-session side rests on licensed tick data: ThetaData's SPXW trade and NBBO tick feed (thetadata.net), which is what a reader would have to subscribe to in order to rebuild the tradable-fill columns, plus the official SPX end-of-day close for settlement. His side uses Cboe 30-minute bars with NBBO quotes and sizes, not the same vendor. Reproducible does not mean free — but the half that carries every statistically meaningful number in this post is the free half.
What is free here is the session archive: each finished session's one-minute index series
and dealer-book levels are in its JSON at
https://gex.live/snapshots/YYYY-MM-DD.json, and the dates are the ones listed at
/sessions. That is enough to rebuild the index-return side of the
delta decomposition — the 10:00-to-close return whose near-zero mean is why the drift caveat
does not bite — though not the option legs.
Part of gex.live research. Measured on the free session archive; every session is free to replay.