Case study 2026

Trading-Colosseum


I set out to build a profitable 0DTE SPY options bot and did not find one. What I built instead was a research system rigorous enough to kill its own hypotheses — 5,640 pre-registered strategy cells, every implementable one negative after costs, each killed on criteria written down before the data was unblinded. The instrument is the result.

Role
Sole engineer and product owner, directing a multi-agent AI team
Stack
Python · pandas / pyarrow · AWS Batch · S3 (Hive-partitioned layers) · ECR · pytest · React 19 · TypeScript · Vite
Headline
5,640 cells tested · 0 implementable survivors · ~12 exit-0 defects caught

The result, stated first

I did not find an edge. Across the whole research arc — 5,640 counted looks at strategy cells, spanning event-day premium, strike-ladder maps, momentum crossovers, behavioural triggers, cross-asset conditioning and the cheap-contract “lottery ticket” question — not one cell is positive after costs, non-anecdotal, stable when its single best session is dropped, and executable by a bot that does not know the future.

That is the honest headline, and it is the one I would lead with in an interview. Most retail algo-trading projects show a curve going up and a Sharpe ratio that is noise. This one shows the machinery that made a negative conclusion trustworthy: hypotheses written down with falsification criteria before the data was unblinded, nulls that account for session clustering, multiple-testing bars quoted next to every p-value, and a lookahead tripwire that fails the build if future data ever becomes visible to a decision again.

  • 647 / 647Sessions scanned2022-03-02 to 2024-12-31. The 2025+ holdout was not read.
  • 311,502Contract-slot rowsOne row per candidate contract at each of 25 entry minutes.
  • $0.00AWS spend on the scanNo Batch job, no queue touched. One 27 MB sync, ~$0.0025 of egress.
  • 0Implementable cellsPositive, non-anecdote, sign-stable, bar-clearing — out of 3,984 counted looks.

Figure

Every grid the study searched returned the same number of implementable survivors.

Horizontal bar chart of cells tested per campaign; the survivors column reads zero on every row.Cells / tests examinedSurvivors06001,2001,8002,400Teenie scan — frozen gridpre-registered cells2,3400Near-money L-2 gridpre-registered follow-up1,6560Teenie scan — POST-HOClabelled post-hoc forever1,6440Event-day cut (H1, Map A)cell × split combinations2560Realized vs implied (L-1)day-gate tests930Behavioural batterytrigger variants320

Cells or tests examined per campaign, against implementable survivors. The first three rows are the arc-cumulative Bonferroni denominator of 5,640 counted looks. Sources: 2026-08-05-teenie-scan-results.md (§5.1, §A.0, §4.2), 2026-08-05-nearmoney-l2-results.md, 2026-08-04-h1-event-day-cut.md, 2026-08-04-behavioral-battery.md.

Show the data
CampaignUnit countedExaminedSurvivorsSource doc
Teenie scan — frozen gridpre-registered cells2,34002026-08-05-teenie-scan-results.md §5.1
Near-money L-2 gridpre-registered follow-up1,65602026-08-05-nearmoney-l2-results.md §head
Teenie scan — POST-HOClabelled post-hoc forever1,64402026-08-05-teenie-scan-results.md §A.0
Event-day cut (H1, Map A)cell × split combinations25602026-08-04-h1-event-day-cut.md
Realized vs implied (L-1)day-gate tests9302026-08-05-teenie-scan-results.md §4.2
Behavioural batterytrigger variants3202026-08-04-behavioral-battery.md

The three largest grids in that chart sum to 5,640, which is the cumulative Bonferroni denominator the whole arc is scored against. Every look is counted. Nothing was examined off the books.

The failure mode that defines the codebase

The hardest bug in this project was never a crash.

A component is missing, dead, or unwired. Nothing raises. A plausible default is returned. The run exits 0 and writes numbers that look reasonable.

Before I trusted a single result, I had the codebase adversarially audited. It found about a dozen defects of exactly that shape. A sample, all catalogued with evidence:

  • The bot-survival loop was a complete no-op — generation two was never seeded from generation one. It ran for weeks. Nothing evolved and nothing complained.
  • Option premium was never debited from cash when a position opened, and options were never settled at expiry. The P&L looked excellent. It was measuring a market where everything is free.
  • Affordability caps were computed and then discarded.
  • update_market_state was never called; one data source loaded only the first day of a multi-day range.
  • 2,507 lines of pattern-detection code never executed once. price_data_for_patterns was set to None and never reassigned, so the type guard downstream always returned “no pattern.” A detector-health run confirmed it independently: 8 of 15 detectors fire, 7 are silent, 0 crash.
  • Seven requested indicators arrived as None forever, because strategies guarded with if x is not None — so signals never fired rather than erroring.
  • Every bot shared one process-global RNG, which made results depend on population composition.
  • A volatility window counted calendar rows, so weekends entered the sample as 0.0 returns.
  • Two lookahead leaks — future data visible to a decision, the classic backtest sin. One gave the fill logic up to sixty seconds of foresight; the other let a session’s close leak into that session’s own bars.

Both leaks are fixed and permanently tripwired by a regression suite that fails if either shape ever returns. The rest were fixed or deleted.

The rule that came out of the audit is now the house rule:

Assume any component you have not personally traced end-to-end is not running. Prove claims by instrumenting a real run.

That is not a slogan; it is a measured position. Reading the code produced four consecutive wrong hypotheses about one quote loader. Two P&L predictions I made from inspection were wrong in direction. Instrumented runs settled both in minutes. Evidence beats inspection, and the corollary is that a green checkmark is evidence of nothing in particular.

How I ran the AI team

This is the part I would hire myself for. I am a software engineer, not a trading expert, and the repo’s own docs say so. The project is an exercise in managing AI agents like an engineering org on a problem where the model can be confidently wrong.

Role Tier Rule
Product owner me Gates all irreversibles: spend, baselines, merges to main
Architect / reviewer of record top tier Direction, briefs, final gate. Judgment only — never bulk work
Senior implementer Opus Behaviour changes, invariants, tests
Senior reviewer Opus, separate instance Line-level review. Never reviews its own work
Junior mechanic Sonnet Exact-spec mechanical work only. Stops on any ambiguity
Observer Haiku Read-only status and ledger arithmetic

Four principles carried the weight.

Author is never reviewer, enforced structurally. Every behaviour change is reviewed by an instance that wrote none of it. The reviewer does fresh mutation spot-checks — deliberately break the code, confirm a test notices — rather than trusting a green suite. The split is not a capability judgment. Every participant in this project, including the architect’s own directives and the auditor’s own document, has been factually corrected by that separation at least once.

Routing rule: the lowest tier that can verifiably do the job. In a codebase whose failure mode is silent degradation, “verifiably” is load-bearing. A junior producing a plausible-looking wrong result is worse than no result, so the mechanic only receives tasks whose success is checkable by a diff, a table or an exit code. If a brief needs the word “decide,” it is a senior task.

Durable state lives in the repo, not in transcripts. Audit spec, changelog, session log, commits. That is what makes a fresh agent spawn per task affordable, which in turn is what keeps the expensive tier spending its tokens on judgment instead of on replaying history.

Human in, on, and out of the loop — placed deliberately. In the loop for irreversibles: spend caps, baseline re-captures, merges. On the loop for overnight delegated runs: hard budget caps and a morning report. Out of the loop wherever the mechanics are verified — pre-registration removes human and model bias at verdict time, because the verdict is computed rather than argued.

Autonomy was budget-guarded. Agents submitted real AWS Batch jobs overnight under a hard project spend cap of $85, with a known-broken cost estimator explicitly distrusted in writing until it was fixed. The routing policy that let short jobs use interruptible capacity was not adopted on vibes: it was adopted after a designed validation experiment — 48 spot jobs across four waves, four reclaims, every one healed by retry, sixteen of sixteen control twins bit-identical, zero capacity stalls — with a pre-registered verdict row that read “adopt for the short rung only; do not extend.”

The pipeline caught its own failures. The defect list above was found by this process, not despite it. One instrument did most of the work: a log-template analyser that groups warnings by shape rather than by occurrence, so the complete set of failure modes is visible at once instead of whichever one scrolled past. That is literally how the dead subsystems were found. A new warning template means the change is not finished.

How the backtest earns trust

Costs are charged before signals are consulted. Entries fill at the NBBO ask, exits at the NBBO bid — the spread is paid both ways — plus $0.65 per contract each way, whole contracts only. At the cheap end of the ladder that toll alone is the whole story.

Figure

Before any signal is consulted, the toll wants the contract mispriced by 12% to 230%.

Dot plot of required mispricing by contract ask price, from 12.1% at ten cents to 230% at one cent.NBBO askRequired mispricing to break even0%60%120%180%240%$0.01230%$0.02$0.03$0.04$0.05$0.06$0.07$0.08$0.09$0.1012.1%

Median required mispricing to break even at each recorded NBBO ask, entering at ask with $0.65/contract commission — 311,502 contract-slot rows over 647 sessions. The modal recorded contract is ask $0.01 with no bid at all. Source: 2026-08-05-teenie-scan-results.md §1.1.

Show the data
AskRowsRequired mispricingSpread crossing only
$0.0183,125230%100%
$0.0273,21576.7%33.3%
$0.0332,10846%20%
$0.0419,45432.9%14.3%
$0.0513,27325.6%11.1%
$0.0610,10820.9%9.1%
$0.078,12517.7%7.7%
$0.086,97915.3%6.7%
$0.095,64113.5%5.9%
$0.105,14812.1%5.3%

The research memo had estimated a required-mispricing band of +12% to +94% before any signal could matter. The measurement disagreed with the memo in the direction that hurts: the median is +77%, and 32% of the recorded ladder sits at ask $0.01 with no bid at all, where breaking even needs the physical payoff to beat the risk-neutral one by 230%. The memo’s most pessimistic row was more optimistic than the median of what is actually there. That correction is in the results document, next to the estimate it replaced.

Pre-registration is the instrument, not the ceremony. Factor lists, exit variants, null models, minimum-firing rules and skew-honesty rules are frozen and committed before the data is unblinded. Anything added afterwards carries a POST-HOC label permanently and is scored against the combined denominator rather than the one it did not earn. The confirmatory study froze twelve registered values at a named commit before it ran.

Nulls are matched, and clustered correctly. A conditional claim has to beat time-matched random purchases, not zero. Read literally as row-level draws, the frozen null was broken: a cell’s 4,574 rows can come from 23 sessions, so a row-level twin draws 4,574 independent days and its sampling distribution is about fourteen times too narrow. It pinned two thirds of the grid to a 0 or 100 percentile and declared hundreds of cells significant at z as high as 78. The fix — session-clustered, side-matched twins — is documented alongside the failure it corrects, and saturation fell from 66% to 21%. The broken version is still in the artifact, labelled, because deleting it would delete the evidence.

Power and skew are stated, not assumed. Cells with fewer than thirty firings are labelled anecdote and cannot be candidates. Every table carries hit rate, mean win, the share of the gross win pot in the three largest wins, and the mean recomputed with the single best session dropped. If one day flips the sign, the headline says so.

That last rule is what killed the study’s most promising-looking result.

Figure

The best abstain gate makes the loss smaller. It never crosses zero.

  • Trade every session
  • Refuse the worst third
Dumbbell chart: expected value moves from minus 65.5 to minus 50.0 dollars per 100 staked, and the zero line is never reached.Dollars per $100 staked-100-75-50-250Break evenTrade every session647 sessions, hold to expiry-65.5Refuse the worst third of daysprior-day range in the top tercile — 216 of 647 days refused-50.0

Expected value per $100 staked on cheap 0DTE contracts, before and after refusing the worst third of sessions. Bands are session-clustered ±2 standard errors. Source: 2026-08-05-teenie-scan-results.md §3.1 and §2.

Show the data
Condition$ per $100±2 SEDetail
Trade every session-65.4932.22647 sessions, hold to expiry
Refuse the worst third of days-49.9648.31prior-day range in the top tercile — 216 of 647 days refused

The equal-priority deliverable was never the entry signal; it was the abstain gate — which days the bot should refuse. It exists, and it is small. Refusing the third of sessions with the widest prior-day range moves expected value from −65.5 to −50.0 per $100 staked, and the band on that number is wide enough to be honest about: ±48.3 at two session-clustered standard errors, and it was the best of twenty-nine looks. Refusing days makes the loss smaller. It does not make it a gain. There is no subset of days on which this bet is not a loss.

Twenty cells in the frozen 2,340-cell grid do clear the side-matched null by more than the Bonferroni bar — but “survives” is a statistical statement with no sign in it, and most of those cells are merely less bad than random. All twenty are the same finding wearing different clothes: the near-money end of the ladder, exited at a fraction of the session’s peak bid. A peak whose minute you do not know until the session is over is an oracle, not an exit. Of the fifteen cells with a positive mean under an exit a bot could actually execute, fifteen of fifteen flip sign when their single best session is removed.

The post-hoc pass reached the same wall from the other side: 1,644 additional cells, 72 of them clearing the combined bar, 54 of those with a negative mean, and zero surviving all four filters. Of the fourteen positive, implementable, non-anecdotal cells, all fourteen flip when one day is dropped — and for twelve of them it is the same day, one December afternoon appearing fourteen times under different names.

Determinism is enforced, not hoped for. Scenario runs must be byte-identical; baselines are re-captured only with my explicit approval, because a model must not approve its own behaviour change. When the scan artifact was widened to support a new exit variant, the 2,340 frozen cells were recomputed on the new data and came back identical, column for column — which is what makes the side-by-side comparison a comparison rather than two different studies.

What I actually learned about 0DTE options

  1. The spread is the strategy killer, and it arrives before the statistics. At the cheap tier the quoted spread plus commissions demands a 12% to 230% mispricing before any signal matters. Most retail 0DTE strategies die here and never find out why.
  2. Rare-event strategies are unfalsifiable at retail sample sizes. A gate that fires 27 times in 713 sessions cannot be distinguished from luck. Not “hard to distinguish” — mathematically cannot, at any reasonable power.
  3. Lottery-shaped payoffs break naive statistics. When the P&L is carried by two or three days, means and bootstrap intervals mislead. The unconditional cell here earns its entire gross win pot from 0.81% of bets — one in 124 finishes in the money. Decomposition into hit rate × payout, top-3 share and a drop-best-day test is mandatory, not optional.
  4. Settlement mechanics are a real landmine. SPY is physically settled with $0.01 auto-exercise, so a “winning” $100 lottery position held to the bell becomes roughly $540,000 of unintended stock exposure per ten contracts. The cash-settled index option fixes it at about 2.9× the crossing cost.
  5. Implied moves are calibrated. Across 647 sessions, 93 day-gate tests on the realized-versus-implied move ratio produced zero survivors against a Bonferroni bar of 0.00054. Option prices already know how much the index moves, including on scheduled-event days.
  6. The unit you measure in can reverse your answer. Scoring per contract and scoring per dollar of premium at risk disagree about the same pre-registered claim, and the disagreement is arithmetic rather than statistical.
  7. Index concentration is not predictability. Knowing that the largest names carry a third of the index’s cap weight tells you almost nothing extra, because the components co-move on one macro factor and the number of effectively independent bets is small. That was my own idea, offered for honest demolition, and demolished.

Number six is the one I would put in front of another engineer, because it is the shape of mistake that survives a code review.

Figure

The same pre-registered claim passes or fails depending on the unit it is scored in.

  • Morning slots
  • Rest of the day
Slopegraph: the morning slots rank 21.8 of 25 scored per contract and 3.7 of 25 scored per dollar of premium.Scored per contractScored per $100 of premium1510152025Rank (1 = best)21.83.7Morning slots10.215.9Rest of the day

Mean rank of the morning slots among all 25 entry minutes, under two scoring units. A 0DTE option's premium decays through the session — median entry cost falls from $145 at 09:45 to $47 at 15:45 — and the correlation between a slot's median entry cost and its per-contract P&L is −0.84. Source: 2026-08-05-nearmoney-l2-results.md §0 and §0.1.

Show the data
GroupMean rank, per contractMean rank, per $100 premiumVerdict on the frozen claim
Morning slots21.8 / 253.7 / 25FAILS per contract · HOLDS per premium dollar
Rest of the day10.2 / 2515.9 / 25

A 0DTE option’s premium decays through the session — median entry cost falls from $145 at 09:45 to $47 at 15:45, a stake three times smaller. A three-times-smaller stake mechanically cannot lose as many dollars, so a per-contract gradient is substantially a measurement of premium size: the correlation between a slot’s median entry cost and its per-contract P&L is −0.84. The same pre-registered claim fails under one unit and holds under the other. It was caught before the conclusion shipped, and both units are reported side by side. Neither reading changes the verdict — L-2 is not supported in either — but a study that had only computed one of them would have shipped a finding it had not earned.

The capstone: an educational trading platform, built overnight

Kour is the spin-off, and it is the clearest demonstration of the harness working at speed: a complete MVP built in a single supervised overnight run under author-is-not-reviewer discipline — React 19, TypeScript in strict mode, Vite, a deterministic simulation engine with a single sanctioned clock call enforced by a purity test, a persona/instinct/dial abstraction, and an LLM coach invoked only at episode boundaries, which is what makes the economics of an AI trading tutor work at all.

The point of the product is the same as the point of the research: a bot is compared to 200 luck twins — same market, random decisions — and the verdict is a signed sentence rather than a flattering chart. The demo state ships a trader that is worse than a coin flip, and the app says so in a full sentence.

639 tests came out of that night. More usefully, the review stage caught three silent-degradation specimens live during the build — including a verdict tier that was sign-blind and rendered “notable” over a bot losing to all 200 of its luck twins. The same failure shape, in a different language, caught by the same discipline.

Stack

Area What I used
Research engine Python, pandas, pyarrow, Parquet, deterministic seeded simulation
Compute AWS Batch with ECR containers, on-demand by default, interruptible capacity only under a ratified policy
Storage S3 with Hive-partitioned layers, in-region only, a local mirror that makes most research runs free
Market data Licensed NBBO option quotes, minute equity candles and volatility index across 1,039 usable 0DTE trading days
Verification pytest, golden-master determinism harness, no-lookahead tripwire suite, mutation-verified tests, log-template analytics
Accounting Spend-ledger arithmetic against a hard project cap
Kour React 19, TypeScript (strict), Vite, Zustand, lightweight-charts, Vitest, Playwright e2e with a network-guard self-test

No market data is republished here. Every figure on this page is a derived or aggregate statistic from the project’s own results documents.