Bilbo Box v2 — replication receipts

On Sept 21 a reader rebuilt this study from my own rules page, on the same 20 names and the same 2019-2026 window, with real options data. Everything matched except the trade count: he had about 260 where my page said 1,031. He was right that something was off, and the error was mine in both directions. This page is the signal spec exactly as the code runs it, the count at every stage of the funnel, what each way of reading a rule does to that count, all 1,374 trades, and a download that reproduces the lot.
20 megacap names2019-01-04 → 2026-07-01hourly ETH grid~1 strike OTM, 28 DTE callbuilt 2026-09-21
The correction. The rules as my page words them, and as the live paper bot codes them, produce 1,374 trades, not 1,031: +5.95% per trade, 40.0% win rate, 1.36 profit factor, t 2.19, 13.8x at 4% of account per trade, worst drawdown -53%. The old 1,031 headline read +11.85% per trade at 1.62 PF because it still applied a volume floor the rules dropped on 2026-07-13 and lost 559 more signals to a feature that was never computed for them. The corrected system is a good deal weaker than the headline, and it is the one the paper account has been trading since July 9.

📦 Full reproduction package

box_signals.py is the whole signal engine in one file, every disputed rule exposed as a flag; it reproduces the published entry list bar for bar. run_variants.py generates the count table in section 8, build_stats.py everything else. reference/ has all 1,374 trades, the 10-variant signal counts, and the base signal list with the 21 EMA feature recomputed. Option quotes are licensed ThetaData NBBO minute data and are not included; 5-minute bars are not either. The engine runs on any 5-minute OHLCV frame.

⬇ download full package.zip
Trades
1,374
2019-01-04 to 2026-07-01
Mean per trade
+5.95%
median -4.6%
Win rate
40.0%
1.36 profit factor
t (day-clustered)
2.19
noise band +2.47% to +9.54%
Growth at 4%/trade
13.8x
worst drawdown -53%
Worst spread exec
6.3x
+4.51% per trade
Premium wiped
0.29%
worst trade -98%
Top 5 winners
38%
of all profit
1 · The signal, exactly

Bars. 5-minute bars from 04:00 to 19:55 ET, resampled to 60 minutes on the on-the-hour grid, extended hours included. The oscillator is computed on that series. Regular-hours-only bars are a different series and give a different answer; section 8 measures how different.

Compression (the grey candle). Saty Phase Oscillator compression: grey when the 21-period Bollinger band on the price side sits inside the 2x ATR14 Keltner band, except when the gap between them is not shrinking and price is already inside the 1.854x ATR expansion zone. ATR is Wilder (RMA). The stdev is pandas' sample stdev; Pine's ta.stdev is the population one and is grey slightly more often.

The box. The high and low of the first 5 grey candles of a compression run, or fewer if the run ends sooner. After the 5th grey candle the box is frozen. The run stays live for 10 calendar days.

The entry, as the rules say and the live bot codes it. The first candle that comes out of compression must close above the box high. That is the spec on this page and the 1,374-trade cohort. Long only.

The entry, as the backtest actually coded it. The published run armed when the bar's high took out the box and then set direction from the close against the box midpoint. That is looser: 574 of the 3,833 long in-window signals poke above the box and close back inside it. Both readings are in section 3.

One shot per compression run. Only the first candle out of compression is tested. If it does not take out the box, that run is over and no later candle re-arms it. Reading it the other way roughly doubles the count (section 8).

Window. Only signals whose hourly close lands 10:00-15:00 ET, Monday to Friday.

Gates. Stock at or above its daily 21 EMA as of the prior completed RTH close. Two-sided option quote no wider than 5% of mid. No volume gate -- the floor was dropped on 2026-07-13 and the live bot does not use one.

Position. One call, strike nearest spot + 0.75 x prior daily ATR14, nearest expiry in the 21-37 day window. 4% of the account in premium for the curves here.

Exits, keyed to the stock. 5-minute close below the box low, sell. Once the stock has run +1.0 x daily ATR in favour, sell on any 5-minute close at or below entry + 25% of the peak gain. Time cap 10 trading days. Median hold 5 calendar days, 90th percentile 14; the cap bound on 265 of 1,374 trades.

2 · The funnel14,503 → 1,374
Hourly compression episodes on the 20 names, 2019-01-01 to 2026-07-06, down to tradable entries. Every replication argument lives in one of these rows.
Stagen of all
Compression episodes that produced a box break (both sides, all hours)14,503100%
Hourly close lands 10:00-15:00 ET, Mon-Fri7,23150%
Long side (breaks below the box are not shorted)3,83326%
The close itself is above the box high3,25922%
Stock above its daily 21 EMA as of the prior close2,45717%
A two-sided quote exists on the ~1-strike-OTM 28-DTE call1,72112%
That quote is no wider than 5% of mid -> the live spec1,3749%
3 · Why my own page said 1,031
Three separate things narrowed the published cohort below its own stated rules. Growth is $1 compounded at 4% of account per trade, half-spread fills.
CohortTradesMean/trade MedianWinPFtGrowthMax DD
the old headline1,031+11.85%-6.7%41.3%1.623.5666.05x-43%
Volume floor still on, the 21 EMA feature missing on 559 signals, no spread cap applied.
no 21 EMA gate1,767+5.74%-5.2%39.3%1.342.2525.67x-66%
Spread cap on, volume floor off, trend gate off.
break on the bar high1,593+5.43%-5.1%38.2%1.331.9414.87x-68%
21 EMA + spread cap, armed the way the backtest actually coded it.
close above the box1,374+5.95%-4.6%40.0%1.362.1913.76x-53%
The spec on this page: the rules as written and as the live bot codes them.
close above the box + volume floor711+13.50%-4.1%44.7%1.773.5129.78x-23%
Shown because this gate is what flattered the old headline.
What went wrong, in order. (1) The headline cohort applied f_hourrel >= 1, a floor on the breakout hour's volume against its own 20-session same-clock-hour median. The rules text dropped that gate in July and the live bot never applies it. (2) The 21 EMA distance was inherited from an earlier cohort that only covered 3,087 of the 3,833 long signals, so d21dist >= 0 silently threw away 559 signals whose feature was simply absent. It is recomputed here for all of them. (3) The stated 5%-of-mid spread cap was never applied to the published cohort at all; it is applied here.
4 · Every trade1,374 trades
Return is on the option premium, entry to exit, filled halfway between mid and the ask buying and mid and the bid selling. Stock prices are on the split-adjusted series, so old levels are not literal prints. Cumulative growth recomputes for whatever filter you pick.
⬇ Download full CSV
Filter All Winners Losers 2019-2022 2023 → 2025 → 2026 One open per name
Growth of $1 at 4% of account per trade, rows selected above
Showing the first 400 rows of the current filter. The CSV has all 1,374.
5 · Windows
The live spec in five windows. Both halves of the record are positive; the first half is not significant on its own.
CohortTradesMean/trade MedianWinPFtGrowthMax DD
2019 -> Jul 2026 (full)1,374+5.95%-4.6%40.0%1.362.1913.76x-53%
2019 - 2022624+3.83%-3.4%42.8%1.301.042.18x-44%
2023 -> Jul 2026750+7.72%-8.6%37.6%1.391.956.33x-49%
2025 ->234+14.10%-13.5%36.3%1.651.453.03x-29%
2026 YTD29+11.84%+0.4%51.7%1.540.841.13x-13%
6 · Year by year
Top-5 share is the fraction of the year's total return carried by its five best trades. A year over 100% is a year where a handful of trades paid for everything else.
YearnMean/tradeWinPFGrowthMax DDTop 5
2019167+4.12%42%1.391.25x-18%211%
2020140-0.48%46%0.950.96x-22%--
2021189+14.83%47%2.332.84x-12%56%
2022128-8.08%34%0.600.64x-39%--
2023282+9.76%38%1.542.57x-47%77%
2024234-1.12%38%0.950.81x-49%--
2025205+14.42%34%1.672.69x-29%84%
202629+11.84%52%1.541.13x-13%242%
Three of the eight years lost money: 2020 flat, 2022 at -8.08% per trade, 2024 at -1.12%. In the winning years the top five trades carry most of the return, 2019 and 2026 more than all of it. That is the shape of the thing: a loose trail paying for long strings of small losers.
7 · By name
TickernMean/tradeWinPFBest
NVDA106+0.64%51%1.10176%
NFLX104+1.33%43%1.29150%
AMD102+19.91%41%2.04429%
GOOGL99-2.53%42%0.84342%
AAPL93+3.07%46%1.18333%
AMZN89-0.27%38%0.98486%
MSFT85+16.58%36%1.84407%
AVGO84-2.22%34%0.66175%
TSLA79+9.03%46%1.72575%
PLTR68+8.95%44%1.37726%
INTC65+9.41%38%1.41537%
MU64+19.26%31%1.83623%
JPM60-9.26%30%0.64241%
UBER54-1.81%22%0.94381%
BAC52+7.53%42%1.38239%
DIS51+42.82%43%3.36670%
META46-2.58%48%0.88176%
SMCI26-6.65%35%0.67116%
HOOD25-6.36%24%0.77366%
COIN22+3.28%32%1.15309%
8 · What each reading of a rule does to the count
Same engine, one flag changed at a time, same universe and window. The last column is tradable trades: exact where I have quotes for that signal set, otherwise the live spec's own quote-and-spread pass rate of 56% applied to the gated count.
ReadingBox breaksIn windowLongAbove 21 EMATrades
break on the bar high (what the backtest did)14,5037,2313,8332,8161,593
regular-hours bars, grid still on the hour6,8606,2633,2862,4721,382 est
close must clear the box high (rules + live bot)12,1426,0743,2592,4571,374
the box must complete all 5 grey bars8,7033,8092,0121,566876 est
any later bar out of compression may trigger21,75410,0275,2733,8942,178 est
population stdev in the oscillator (Pine ta.stdev)14,7317,3273,9002,8631,601 est
30-minute bars instead of hourly30,03016,6538,3125,7073,191 est
regular-hours bars, grid anchored at 09:306,3165,7773,0482,2951,283 est
regular hours + strict close + 5 grey bars3,6723,3091,7391,395780 est
09:30 grid + strict close + 5 grey bars3,3573,0471,5861,269710 est
Nothing here gets you to 260. The strictest stack on the list -- regular-hours bars on a 09:30 grid, a strict close above the box, and a full 5-candle box -- still leaves about 710 tradable trades. Getting to 260 needs one of those readings and an option chain that only fills about a fifth of the signals, which is what an end-of-day or thin-strike quote source does to a 5%-of-mid spread cap. The grid choice matters far less than I would have guessed: every session and grid variant lands between 2,295 and 2,816 gated signals.
9 · Do the gates earn their keep
Each gate held open, then replaced by a random gate that fires at the same rate, 30 seeds. The noise band is a bootstrap of the live spec itself: +2.47% to +9.54% per trade.
CohortTradesMean/trade MedianWinPFtGrowthMax DD
no gates at all2,265+5.48%-7.1%37.4%1.291.6441.16x-76%
21 EMA only1,721+5.34%-6.5%37.9%1.291.9215.95x-63%
spread <= 5% only1,767+5.74%-5.2%39.3%1.342.2525.67x-66%
21 EMA + spread (the live spec)1,374+5.95%-4.6%40.0%1.362.1913.76x-53%
live spec + the dropped volume floor711+13.50%-4.1%44.7%1.773.5129.78x-23%
GateFires onRealRandom medianRandom 5-95%Verdict
21 EMA76%+5.34%+5.66%+3.06% .. +6.56%inside band, free parameter
spread <= 5%78%+5.74%+5.51%+3.02% .. +6.52%inside band, free parameter
volume floor (dropped)52%+13.50%+5.57%+2.94% .. +8.08%outside band, real work
This is the uncomfortable part. Neither gate the live system uses beats a random gate firing at the same rate. The 21 EMA gate and the spread cap both sit inside the random band, which is the definition of a free parameter. The one gate that is clearly outside the band is the volume floor -- the gate I dropped. I am not re-adding it on the strength of one table on a record it was chosen from; it goes on the list to test on a window it was not read from, and the live sleeve keeps running without it. But the old headline's +11.85% per trade was mostly that gate, and saying so is the point of this page.
10 · Fills and costs
Half-spread is the page default: buy at mid plus half the spread, sell at mid minus half. Full-spread is the pessimistic rerun -- pay the whole spread in both directions, every trade.
WindowHalf meanHalf PFHalf growthFull meanFull PFFull growthGive-up
2019 -> Jul 2026 (full)+5.95%1.3613.76x+4.51%1.266.33x-24%
2019 - 2022+3.83%1.302.18x+2.44%1.181.54x-36%
2023 -> Jul 2026+7.72%1.396.33x+6.24%1.304.10x-19%
2025 ->+14.10%1.653.03x+12.23%1.552.56x-13%
2026 YTD+11.84%1.541.13x+9.17%1.401.10x-23%
Paying the whole spread both ways takes the full record from +5.95% to +4.51% per trade and 13.8x to 6.3x. It stays positive, which is the only thing this table needs to show. Median quoted spread on the cohort is 2.6% of mid, 90th percentile 8.6%.
11 · Calendar completeness
Months covered
91/91
none missing
Distinct trade days
775
2019-01-04 to 2026-07-01
Biggest gap
37 days
after 2020-02-19
Trades per month
15
1 to 39
Every month of the 91 in the window fired at least one trade, so the schedule never goes silent. The one real hole is 37 days after 2020-02-19: compression disappears in a crash, and the box needs quiet bars to form. The quietest month was 2026-02, the busiest 2023-11.
12 · Trade-sequence resample
10,000 paths of 180 trades, about a year at this firing rate, drawing whole trading days with replacement so same-day clusters stay together. 4% of account per trade, half-spread fills.
Median path
1.36x
p25 1.03x / p75 1.85x
5th - 95th
0.70x - 2.95x
23% of paths end below start
Max drawdown
-25%
p95 path -44%
Longest losing run
10
p95 15 in a row
A year of this at 4% per trade loses money about one time in four, and the drawdown you have to sit through on the bad-but-not-terrible path is -44%. Fifteen losers in a row is a normal thing here, not a broken system. Size so the p95 path is survivable or do not run it.
13 · One open position per name
The backtest compounds trades one after another and lets the same name hold several boxes at once. Holding one position per name at a time, using the real exit stamps, blocks 287 of the 1,374.
CohortTradesMean/trade MedianWinPFtGrowthMax DD
every signal1,374+5.95%-4.6%40.0%1.362.1913.76x-53%
one open per name at a time1,087+7.87%-4.6%40.7%1.472.7617.50x-42%
Blocking the overlaps improves every number, which usually means the blocked trades were re-entries into a name that was already going wrong. Worth a proper test on its own; it is not how the sleeve is coded today.
What isn't in this backtest (read before you trust it)

Strike and expiry were chosen on the whole record. The exit rule was picked on 2019-2022 and run untouched on 2023-2026, which is a real out-of-sample test. The ~1-strike-OTM 28-DTE cell was not; treat the exact per-trade figure as flattered.

The curve compounds trades single-file. In reality several positions are open at once, so the path is a simplification in both directions. Section 13 is the nearest honest correction.

Fills are quote-based, not filled orders. Entry and exit prices come from ThetaData NBBO minute quotes at the actual timestamps, marked half-spread. No partials, no queue, no assignment, no dividends. Calls are always sold, never exercised.

Exits are evaluated on 5-minute closes of the underlying, and the live bot only looks once per run, so real exits lag the modelled ones. That lag is logged live but is not in these numbers.

Long-only megacap momentum. 2020 was flat and 2022 was the worst stretch. Expect it to bleed in bear markets, and expect the typical trade to lose money: the median trade here is -4.6%.

No live track record. The forward paper record accrues at /bilbo-paper.html. It is being generated by the no-volume-gate version, which is the 1,374-trade spec on this page, not the old headline.