Strategies
Earnings-Gap Continuation (PEAD Proxy), Long-Short Event-Driven Basket on 20 Liquid US Large Caps (USEQ 1-DAY — enter at the CLOSE of a >=3-sigma high-volume overnight gap, ride the post-announcement drift for ~15 sessions, 3-parameter)
Outcome: Abandoned
UsLargeCapEarningsGapContinuationLS
Outcome Summary
UsLargeCapEarningsGapContinuationLS traded post-earnings drift on 20 US large caps, entering at the close after a high-volume overnight gap of roughly 3 sigma or more and following the gap's direction. The optimization on record looked promising: 58.65% return, Sharpe 1.28 over 148 trades, 4.20% average per-trade return, a passed holdout (Sharpe 1.52) and PBO 0.27. It did not pass the programme-wide FDR check, however, and nearly all trades were long (133 of 148). On iteration 8 the code failed the Layer 3 sandbox backtest by running past the 300s limit, and the run was abandoned before any analyst or risk verdict.
Hypothesis
verification_loop: sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of META.USEQ-1-DAY-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).
Implementation
Event-driven long/short basket that trades post-earnings drift on 20 liquid US large caps (USEQ, daily bars). Each session it looks for any name whose overnight gap is at least gap_sigma_threshold (default 4) times its 60-session daily sigma, on at least 2x average volume. It enters at the close of that day in the gap direction. A position closes at the first of: the gap fully filling (close back through the pre-event close), a new opposite gap of at least 2 sigma on at least 1.5x volume, a 5-session time stop if the return is below 0.5x the entry sigma, or 20 sessions. Size is equity x 7.5% x clamp(1.8% / sigma, 0.5, 1.5), with at most 10 names and gross capped at 1.5x equity. Long-leg and short-leg realised returns and exit-reason counts are logged on every exit.
Verification Results
sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of META.USEQ-1-DAY-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).
No code change required for this hypothesis. If the analyst wants to bound the effect, re-run with _volume_mult raised slightly (e.g. 2.2) and confirm the trade count and per-trade return are stable — the gate should not be sitting on a cliff.
Execution-timing realism of the volume gate. The event test requires volume_ratio = volume_t / mean(prior 60 volumes) >= 2.0, where volume_t is the FULL session's volume, and the entry is submitted on that same bar so it fills at that same session's close. This is exactly what the hypothesis pre-registers ('enter at the CLOSE of the event day'), and it is NOT look-ahead in the engine's terms (the bar is close-stamped, gap_z uses only the session open and the strictly-prior 60 closes, and sigma/avg_vol are computed BEFORE today's return and volume are appended to the deques, so no current-bar leakage into the z-score). The realism caveat is that a live implementation must commit a market-on-close order several minutes before the bell, at which point the day's volume is ~95-99% final, not 100%. For a 2x-average-volume gate on an earnings day this will almost never flip the decision, but a handful of marginal events near the threshold would differ live.
Either store the session TIMESTAMP instead of the index in _entry_sess (self._entry_sess[iid] = self._session_ts[-1], then bisect it like the ts_opened path), or subtract the trim offset from every stored index when _session_ts is truncated.
Latent index-staleness in the _sessions_held fallback path. self._entry_sess stores an ABSOLUTE index into self._session_ts, but calculate_signal trims that list (when len > 600 it is replaced by the last 400 entries), which shifts every index by 201 without rewriting _entry_sess. A stored index from before a trim would make (n - 1) - start negative, and max(0, ...) then resets the holding clock to zero, so a position could be held indefinitely. This path is currently unreachable: it is only taken when pos.ts_opened is 0/absent or older than _session_ts[0], and NautilusTrader always populates Position.ts_opened in backtest, paper and live, while a 400-session retained grid is ~26x the maximum hold_days of 30. Flagging it as latent rather than active.
Analysis
Outcome Summary
A multi-leg daily basket must keep per-bar work at O(log n) or better (compute arrays once, use bisect, cache lookups), or verification will time out no matter how strong the edge looks. Its lopsided long/short mix (133 vs 15 trades) should be checked before separate thresholds are tuned for each leg.
After 8 iterations the run was abandoned in the verification loop. The Layer 3 sandbox backtest timed out, exceeding the 300s wall-clock limit on the ~20,000-bar window (2,400 days of META.USEQ daily bars), so the latest version never reached backtest review, analysis or risk review. No analyst or risk verdict was recorded.
Capture post-earnings-announcement drift on 20 liquid US large caps (USEQ, daily bars) without an earnings calendar: find overnight gaps of roughly 3 sigma or more on at least 2x average volume, enter at that day's close in the direction of the gap (long or short), and hold for a fixed number of sessions.
The optimization report on record (2012-05-18 to 2023-12-29) shows a 58.65% total return, Sharpe 1.28, 148 trades (133 long, 15 short), 4.20% average return per trade, 67.6% win rate and 3.35% max drawdown. Walk-forward out-of-sample Sharpe was 1.08, PBO 0.27, and the holdout passed with Sharpe 1.52 on 39 trades, but the strategy did not survive the programme-level FDR check (p=0.0131; BH at q=0.10 kept 4 of 301 candidates).
Backtest Review
- Sharpe
- 0.57
- Total return
- 68.71%
- Max drawdown
- 5.94%
- Trades
- 532
- Win rate
- 57.3%
- Profit factor
- 1.58
Backtest and paper results are hypothetical. Trading involves risk of loss.