Skip to content
All strategies

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

A LONG-SHORT, MULTI-INSTRUMENT, EVENT-DRIVEN continuation strategy on US equities (venue USEQ, 1-DAY bars) that harvests post-earnings-announcement drift (PEAD) WITHOUT needing an earnings calendar: an earnings announcement reveals itself in the price tape as an abnormally large overnight gap on abnormally large volume. The strategy scans a fixed 20-name liquid large-cap universe every daily bar, flags names whose open-vs-prior-close gap exceeds ~3x their trailing 60-session daily volatility on >=2x average……Show moreShow less

A LONG-SHORT, MULTI-INSTRUMENT, EVENT-DRIVEN continuation strategy on US equities (venue USEQ, 1-DAY bars) that harvests post-earnings-announcement drift (PEAD) WITHOUT needing an earnings calendar: an earnings announcement reveals itself in the price tape as an abnormally large overnight gap on abnormally large volume. The strategy scans a fixed 20-name liquid large-cap universe every daily bar, flags names whose open-vs-prior-close gap exceeds ~3x their trailing 60-session daily volatility on >=2x average volume, enters AT THE CLOSE OF THE EVENT DAY in the DIRECTION of the gap (long the gap-ups, short the gap-downs — Reg-T margin allows the short leg), and holds a fixed ~15 sessions. It deliberately does NOT enter at the open (the jump itself is not tradeable) and does NOT fade the gap — the documented anomaly is UNDER-reaction, i.e. continuation. This fills three of the furthest-from-target portfolio buckets at once: USEQ is 1.6% of 3074 experiments, long_short is 14.8%, multi-instrument is 18.5%; it also avoids BINANCE (70.0%, double its 40% cap). It is explicitly NOT the daily ETF regime-rotation / cross-asset dual-momentum family that lesson L99 kills, and NOT the cross-sectional weekly reversal that died before.

All four library refinements were already in the iteration-7 code: the gap-fill and opposite-gap exits, the 5-session time stop scaled by sigma, inverse-vol sizing clamped to [0.5, 1.5], and one symmetric threshold with hold_days fixed at 20 and one searchable parameter. The FDR blocker comes from how strong the Sharpe is relative to the whole programme (4 of 301 kept). No code fix removes it, and adding searchable knobs would make the deflated-Sharpe penalty worse. So trading logic is unchanged. The only change……Show moreShow less

All four library refinements were already in the iteration-7 code: the gap-fill and opposite-gap exits, the 5-session time stop scaled by sigma, inverse-vol sizing clamped to [0.5, 1.5], and one symmetric threshold with hold_days fixed at 20 and one searchable parameter. The FDR blocker comes from how strong the Sharpe is relative to the whole programme (4 of 301 kept). No code fix removes it, and adding searchable knobs would make the deflated-Sharpe penalty worse. So trading logic is unchanged. The only change is refinement 4's reporting request: long-leg and short-leg P&L and exit-reason counts are now logged and stored as persisted scalars, so the legs are judged separately rather than tuned apart. FDR is a soft floor, so a waiver is the analyst's decision. If this exact specification still fails FDR, I recommend ending the loop rather than tuning further. Note: I could not write the outbox file at workspace/outbox/developer/strategy_e0ce2ad3-95ba-4478-a516-a601c8c76554-c10fef5ace.json because the tool permission check failed twice, so this JSON reply is the only output. Also, previous_config was not in the brief, so this config was rebuilt from the code and hypothesis: META primary, the other 19 names as extra legs on USEQ 1-DAY bars, and leverage 1.0.

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

Promotion blocked by numeric gates — address these before promoting: - unwaived soft floors: does not survive programme-level FDR (Benjamini-Hochberg q=0.10 keeps 4 of 301 candidates) ## Library refinements (from the knowledge library; test them, do not assume them) The library has no PEAD research, but its gap theory, stop models, risk-parity notes, overfitting notes and one earnings event-driven example all point the same way. Add a gap-fill/opposite-gap invalidation exit and a 5-session time stop, size……Show moreShow less

Promotion blocked by numeric gates — address these before promoting: - unwaived soft floors: does not survive programme-level FDR (Benjamini-Hochberg q=0.10 keeps 4 of 301 candidates) ## Library refinements (from the knowledge library; test them, do not assume them) The library has no PEAD research, but its gap theory, stop models, risk-parity notes, overfitting notes and one earnings event-driven example all point the same way. Add a gap-fill/opposite-gap invalidation exit and a 5-session time stop, size positions by inverse volatility, and cut the search surface. The goal is a higher Sharpe per parameter so the strategy can survive programme-level FDR. 1. [exit] Gap-fill / opposite-gap invalidation exit: Store ref_close = the close of the session before the event (close_{t-1} on the event day). Longs: exit at the close of any held session whose close < ref_close, meaning the gap has fully filled. Shorts: exit if close > ref_close. Also exit early at the close when a NEW opposite-direction event prints on the same name: a gap of 2.0 sigma or more against the position on volume_ratio >= 1.5, i.e. a gap-down while long or a gap-up while short. Otherwise keep the hold_days exit. All checks use the session close, so there is no look-ahead. — Right now every trade rides the full 23 sessions no matter what. The drift thesis is under-reaction continuation, so a gap that fully closes has disproved it for that event. The FMZ primer says 跳空缺口是日后支撑和压力点的参考价位 (the gap level becomes later support/resistance) and treats a new gap as the start of acceleration in the gap direction. The stop-model article lists 'price makes a downward breakaway gap' as a pattern stop (形态止损) and codes exactly that exit: 'K线出现向下跳空缺口,多头止损' (exit longs on a down-gap bar, shorts on an up-gap bar). Cutting failed events early should lift profit factor (1.58) and Sharpe (0.57) with no new searchable parameter. (source: 发明者量化交易入门--从基础到实战 p.1; 止损模型的原理及编写 p.1) 2. [stop] Early time stop when the drift fails to start: At the close of the 5th session after entry, exit if the position's return in the gap direction is below +0.5 x sigma_entry (the 60-session daily stdev frozen at entry). This works out to about +0.8% for a typical large cap. Positions that pass the check run to hold_days or the gap-fill exit. — The stop-model article describes 时间止损 (time stop) as: if the expected move does not follow soon after the trigger, 'if it doesn't happen, exit', and gives the example 横盘止损 (sideways stop): 买入后5天内上涨幅度未达到5%即进行止损 (stop out if it has not gained 5% within 5 days of buying). It also says this should be paired with a max-loss stop, which the gap-fill exit supplies. This strategy holds all 532 trades for a fixed horizon, so dead-money events tie up the 1.5x gross budget and the 10 slots for ~23 sessions. A volatility-scaled 5-day check frees capacity for new events. Average trade return is 1.38%, far above the 0.10% round-trip cost, so the extra exits are cheap. (source: 止损模型的原理及编写 p.1) 3. [sizing] Inverse-volatility position sizing: Replace flat notional (equity x 0.075) with notional_i = equity x 0.075 x (0.018 / sigma_i), where sigma_i is the 60-session daily stdev at entry. Clamp the multiplier to [0.5, 1.5], so per-name notional stays between 3.75% and 11.25%. Keep the 1.5x gross cap and 10 slots. — The universe mixes names like TSLA, NVDA, AMD and MU (daily sigma about 3-4%) with names like WMT, CSCO and JPM (about 1.2-1.5%). Equal notional lets a few high-vol names dominate P&L variance. The risk-parity summary says capital should be allocated by risk contribution (按照风险贡献分配资金), not by capital share, with each asset's historical volatility (standard deviation) as the main risk measure. Equalising risk per event should raise Sharpe at the same return, and Sharpe feeds the deflated-Sharpe/FDR test that is blocking promotion. (source: 风险平价模型及策略精华汇总 - BigQuant量化交易 p.1) 4. [parameter] Shrink and fix the search surface (one symmetric threshold, fixed horizon): Collapse gap_sigma_threshold_long and gap_sigma_threshold_short into one symmetric gap_sigma_threshold fixed at 4.0. The optimizer placed the short threshold at 7.96, against the 8.0 bound, which means the short leg is effectively off. Fix hold_days at 20 instead of searching [3, 30]. That leaves 0 or 1 searchable parameter; if one must remain, search only gap_sigma_threshold in [3.0, 5.0]. Report long-leg and short-leg P&L separately rather than tuning them apart. — The blocker is programme-level FDR (Benjamini-Hochberg keeps 4 of 301). The deflated Sharpe is penalised by the number of trials and rewarded by a parameter-light specification. The overfitting article warns that a model with too many inputs or too much flexibility memorises noise, and recommends removing inputs whose presence you cannot justify (删除无用特征). A short threshold pinned to its bound is exactly that: an asymmetric fit, not the pre-registered 3-sigma long-short rule. The BigQuant earnings event-driven example (条件选股:基于财报的事件驱动策略) uses a single fixed holding period (持仓周期:30天). That supports pre-committing a horizon in the 20-30 day range rather than searching it; the article's performance is treated as a claim only. (source: 机器学习中的过拟合 - BigQuant量化交易 p.1; 条件选股:基于财报的事件驱动策略 - BigQuant量化交易 p.1)

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.