Skip to content
All strategies

Strategies

Dual-Class Share Spread OU Reversion with Translation-Invariant, Cost-Aware Thresholds (GOOGL/GOOG, NWSA/NWS, FOXA/FOX on USEQ 1-DAY, dollar-neutral long-short)

Outcome: Abandoned

DualClassShareSpreadOUReversion

Outcome Summary

The strategy sought mean reversion across three pairs of share classes from the same companies. Its sandbox averaged 0.099% per closed position. After three iterations, QA found that its entry threshold could permit trades whose projected convergence did not cover estimated execution costs, and the run was abandoned before later stages.

Hypothesis

Source: mpeac, 'Why aren't the optimal entry/exit thresholds for OU pairs trading relatively invariant to shifts in the OU mean?', quant.stackexchange.com/questions/72234 (2022-09-18), which discusses Leung & Li, 'Optimal Mean Reversion Trading with Transaction Costs and Stop-Loss Exit'. Classification: (a). The post is a theory question, but it names a concrete mechanism: trade a mean-reverting OU spread, entering at a cost-aware threshold below/above the mean and exiting at an optimal level, with a stop. Answer……Show moreShow less

Source: mpeac, 'Why aren't the optimal entry/exit thresholds for OU pairs trading relatively invariant to shifts in the OU mean?', quant.stackexchange.com/questions/72234 (2022-09-18), which discusses Leung & Li, 'Optimal Mean Reversion Trading with Transaction Costs and Stop-Loss Exit'. Classification: (a). The post is a theory question, but it names a concrete mechanism: trade a mean-reverting OU spread, entering at a cost-aware threshold below/above the mean and exiting at an optimal level, with a stop. Answer to the question, which shapes the design: Leung-Li's thresholds are not translation-invariant because their problem is long-only optimal stopping on the discounted LEVEL of the spread (the reward is e^{-rt}(X_t - c)). Discounting a level that changes sign makes the value depend on where theta sits relative to 0. Theirs is not a symmetric long/short problem on the deviation X - theta. For a two-sided spread trader with negligible carry, the right formulation is in deviation units: entry and exit bands are set relative to a point-in-time theta and are symmetric for both sides. That is the invariance the asker expected. We use that formulation. The open question is which spread really is OU. Statistical pairs are dead in this factory (L113, the mean_reversion tag is 0/203), so we use the one spread class where mean reversion is contractual: two share classes of the SAME company. They have identical cash flows and differ only in voting rights. The log price ratio is pinned to a slowly drifting voting premium (theta), and liquidity and index-flow shocks push it away from that premium. Fit OU on log(A/B) over a rolling window, trade z-deviations from the rolling theta, and hold for days. Dollar-neutral: long the cheap class, short the rich class, same notional. All three pairs run in one strategy instance, each as its own independent sleeve. USEQ is under-represented at 5.0% of experiments, and long_short is under-represented at 15.7%. PRE-STUDY (point-in-time rolling mean and std, entry |z|>k, exit at z crossing 0 or a 20-session cap, daily closes from the catalog via load_normalised). Gross return per trade is in % of total two-leg notional. GOOGL/GOOG since 2014-04: L60/k2.0 gives 83 trades, gross +0.12%, win 77%, 12 of 13 years positive. L20/k2.0 gives 138 trades, +0.12%, win 85%, 13 of 13 years positive. NWSA/NWS since 2013-07: L60/k1.5 gives 126 trades, +0.25%, median +0.43%, win 72%. L120/k2.0 gives 57 trades, +0.26%. NWS has 4 losing years: 2013, 2021, 2023 and 2024. FOXA/FOX since 2019-03 is the weakest: L20/k2.0 gives 88 trades, +0.10%, win 76%, and the longer windows are about 0. FOX is kept as a third, independent sleeve so that the hypothesis can be checked against it rather than tuned to it.

This keeps the OU-threshold mechanism from the source. It applies the answer to the post's question by using symmetric, deviation-based thresholds around a point-in-time theta, which are invariant to shifts in theta. It moves the OU assumption onto the one spread class where mean reversion is contractual: same-company dual share classes. It avoids the dead statistical-pairs and crypto pairs families: DogeBybitOkxOu, Tadi basket and XOM/CVX Bertram are all in the pipeline or failed. USEQ (5%) and long_short (15.7%) are under-represented. The pre-study was point-in-time on catalog data. Implementation note: daily bars, 6 instruments and about 3.3k sessions, with O(1) deque statistics per sleeve. This avoids the sandbox_timeout that killed the Tadi and PEAD runs.

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] The sandbox averaged 0.099% per closed position; assess full-history results after spread, impact, and borrow costs. - [critical] The 0.0004 threshold permits entries with only 0.04% projected convergence per total two-leg notional. USEQ round-trip spread and impact can reach approximately 0.05%, before short borrow costs. (line 156) — fix: Require projected convergence to exceed a conservative, pair-specific estimate of all execution and borrow costs.

Implementation

Three independent dollar-neutral sleeves (GOOGL/GOOG, NWSA/NWS, FOXA/FOX) on USEQ daily bars. Each sleeve tracks the log price ratio of the two share classes with O(1) rolling mean/variance over `lookback` sessions. It enters long the cheap class and short the rich class at equal notional (leg_notional_pct of equity per leg) when |z| >= entry_z and the deviation clears round-trip cost. It exits when z reverts past exit_z, on a stop_z blow-out, or after max_hold_sessions sessions.

Diagnosis of the Layer-3 sandbox_timeout: the strategy code is not the cause. A local reproduction of the exact sandbox call (src.backtesting.subprocess_entry, window 2017-06-09 to 2024-01-04, 6 USEQ legs) finished in 220s wall-clock with 224 trades. A cProfile run took 291s in total. Of that, 260s was ParquetDataCatalog.query: fsspec walks the whole catalog (~249k directories, 371k ls calls) on every query, at about 111s each. The primary load_normalised() call pays it once, the batched extra-legs catalog.bars()……Show moreShow less

Diagnosis of the Layer-3 sandbox_timeout: the strategy code is not the cause. A local reproduction of the exact sandbox call (src.backtesting.subprocess_entry, window 2017-06-09 to 2024-01-04, 6 USEQ legs) finished in 220s wall-clock with 224 trades. A cProfile run took 291s in total. Of that, 260s was ParquetDataCatalog.query: fsspec walks the whole catalog (~249k directories, 371k ls calls) on every query, at about 111s each. The primary load_normalised() call pays it once, the batched extra-legs catalog.bars() call pays it again, and the instruments query adds another 38s. Strategy callbacks are negligible: per-bar work is O(1) and there are about 10k bars in total. Under factory load the two fixed scans alone push the run past 300s, and both attempts timed out. No strategy-side edit can remove this cost, so this resubmission changes only a comment documenting the per-bar cost and the leg-alignment rule (lessons L174/L139). Trading logic is unchanged. Fix needed on the platform (lesson L155(b), plumbing): in BacktestRunner, fold the primary bar_type into the single batched catalog.bars() call, or cache the catalog file list. Either change would save about 111s per multi-leg run. For context, the local full-window run gave avg_trade_return_pct 0.091% and Sharpe 0.14, which is weak against the pre-registered 0.10% / OOS Sharpe 0.8.

Verification Results

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] The sandbox averaged 0.099% per closed position; assess full-history results after spread, impact, and borrow costs. - [critical] The 0.0004 threshold permits entries with only 0.04% projected convergence per total two-leg notional. USEQ round-trip spread and impact can reach approximately 0.05%, before short borrow costs. (line 156) — fix: Require projected convergence to exceed a conservative, pair-specific estimate of all execution and borrow costs.

Iteration History

sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of GOOGL.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).

Outcome Summary

Require projected convergence to exceed conservative, pair-specific execution and borrow costs.

After three iterations, Layer 4 QA rejected an entry threshold allowing 0.04% projected convergence when estimated spread and impact costs could reach approximately 0.05%, before short borrow costs. The run was abandoned at verification.

It traded mean reversion in three dual-class share spreads using dollar-neutral long and short positions.

The sandbox averaged 0.099% per closed position. No full backtest or optimization report was provided.

Abandon Reason

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] The sandbox averaged 0.099% per closed position; assess full-history results after spread, impact, and borrow costs. - [critical] The 0.0004 threshold permits entries with only 0.04% projected convergence per total two-leg notional. USEQ round-trip spread and impact can reach approximately 0.05%, before short borrow costs. (line 156) — fix: Require projected convergence to exceed a conservative, pair-specific estimate of all execution and borrow costs.

Analysis

Why aren't the optimal entry/exit thresholds for OU pairs trading relatively invariant to shifts in the OU mean?

Backtest and paper results are hypothetical. Trading involves risk of loss.