2026-04-21
50 roll_thesis_intact trades had closed at avg -4.7% across all strategies in the paper book, -24.3% in the all_plays baseline (n=10). Every single one fired within 3-6 days of entry. The uniformity was the tell: a real exit-rule signal wouldn’t cluster that tightly around the minimum-hold guard.
The paper trader (part of an options trading system I built) takes recommendations from a positions engine and books simulated outcomes against historical option prices, so we can measure how each exit strategy actually performs over a freeze window. The engine’s check_single_position returns action='roll' when DTE ≤ 90 and the position is OTM with the thesis intact. The intent of roll is “sell this contract, buy the same strike at a later expiry; the bet is still live, just buy more time.”
What the paper trader did instead was a single-line oversight: any status.action != "hold" fell through to the close block. The engine’s roll got mapped to “close at day-3 mark-to-market.” For any earnings or short-DTE play entered on a ≤90-day option, day 3 is the first check after the minimum-hold guard. If the underlying drifted slightly OTM in those three days, the roll trigger fired and the paper book booked the loss instead of continuing the bet.
This systematically understated baseline all_plays performance. The strategies using unified exit logic (all_plays, high_conviction, spread) all paid for it. The TP/SL variants (exit_tight, exit_wide, exit_trailing) bypass that path entirely, which partly explains why they looked better on simple leaderboard metrics. Fix: treat roll as hold for measurement. Position stays open, gets re-evaluated next day, eventually exits via close_otm_30d, cut_loss, take_profit, or thesis_broken_*. Freeze-compliant, measurement only, no behavior change to user-facing recommendations.
Measurement instrumentation that maps “any non-hold action” to “close” will silently mark every “keep going” decision as a realized loss. The bug isn’t in any one branch; it’s in the default. When a fall-through default exists between an enum and an action, every new enum value is a latent bug until proven otherwise.