Skip to content
All strategies

Strategies

PURR spot–perpetual premium convergence

Outcome: Abandoned

PurrSpotPerpPremiumConvergence

Outcome Summary

PurrSpotPerpPremiumConvergence was a market-neutral basis trade: long PURR spot and short the PURR perpetual on Hyperliquid when the perp premium went above 1.2%. The hypothesis cited 4,172 overlapping 4-hour bars and 69 episodes above that threshold. Over 2 iterations the code never passed verification, because the Layer 3 sandbox backtest timed out at 300s on a ~20,000-bar window. The run was abandoned before any backtest, optimization or analyst review, so the premium-convergence thesis is still untested.

Hypothesis

On synchronized 4-hour bars, buy PURRUSDC.HYPERLIQUID_SPOT and short equal notional PURRUSD.HYPERLIQUID when the perpetual premium exceeds 1.2%. The catalog contains 4,172 overlapping bars; 69 separate episodes crossed that threshold before liquidity filters. Include actual hourly funding payments. Hyperliquid funding mechanics: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/funding

Hyperliquid, multi-instrument, and long-short strategies are underrepresented. PURR's observed premium is wide enough to test a paired convergence trade. Compression from 1.2% to 0.3% yields 0.9% gross per matched notional. Estimated two-leg round-trip fees are 0.23%; allowing another 0.20% for spread and impact leaves 0.47%, or about 0.235% of total paired notional. Executable depth must be verified.

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 PURRUSD.HYPERLIQUID-4-HOUR-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

On aligned 4H bars, when the PURR perp premium over HL spot is >= entry_basis_pct, buy PURRUSDC.HYPERLIQUID_SPOT and short equal notional PURRUSD.HYPERLIQUID. Each leg is its own order and position. Exit both legs when the premium compresses to exit_basis_pct, widens by spread_stop_widening_pct above the entry premium, or after max_hold_bars bars (anchored to entry timestamp). Leg notional is the minimum of the risk budget (risk_pct / stop width), max_leg_equity_fraction of equity, and max_prior_bar_volume_fraction of each leg's prior-bar dollar volume. Hourly HL funding is accrued by the engine on the perp leg.

Iteration 2 answers a Layer-3 sandbox_timeout. I ran the exact sandbox config locally under cProfile and py-spy on 2024-04-17 to 2026-04-17. Total wall time was 403 s. About 385 s went to NautilusTrader ParquetDataCatalog.query, mostly fsspec glob/walk over the whole catalog: 2.46M stat() calls across 17 queries, on a host with the disk at 95%. That covers catalog.bars at 336 s and catalog.instruments at 51 s. All strategy code (calculate_signal, should_enter, should_exit, position_size, _submit_entry) took under……Show moreShow less

Iteration 2 answers a Layer-3 sandbox_timeout. I ran the exact sandbox config locally under cProfile and py-spy on 2024-04-17 to 2026-04-17. Total wall time was 403 s. About 385 s went to NautilusTrader ParquetDataCatalog.query, mostly fsspec glob/walk over the whole catalog: 2.46M stat() calls across 17 queries, on a host with the disk at 95%. That covers catalog.bars at 336 s and catalog.instruments at 51 s. All strategy code (calculate_signal, should_enter, should_exit, position_size, _submit_entry) took under 0.25 s for 3,171 processed bars; base _maybe_process_primary took 0.65 s. Per-bar cost is already O(1): no history scans and no supplementary rescans. That run produced 100 position records (50 paired entries), Sharpe 3.31 and avg_trade_return_pct 0.70%. Per lesson 155(c), the timeout is not per-bar cost that grows with the span, so no strategy edit can fix it. It is catalog-load latency in the runner and needs an engineering fix (cache the file list or narrow the catalog glob) or a retry on a less loaded host. The only code change is a behaviour-preserving refactor: the duplicated spot-quantity rounding is moved into _spot_qty_for_notional(). Signal, gates, sizing, exits, parameters and the optimization plan are all unchanged.

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 PURRUSD.HYPERLIQUID-4-HOUR-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

Multi-leg strategies must keep per-bar work in calculate_signal/on_bar at constant or O(log n) cost, for example by precomputing or caching lookups, so they finish the sandbox smoke test within its time budget before their edge can be measured.

It was abandoned in a verification loop after 2 iterations. The Layer 3 sandbox smoke test went over its 300s wall-clock limit on a ~20,000-bar window (2,400 days of PURRUSD.HYPERLIQUID 4-hour bars), so backtest review, optimization, analyst and risk stages were never reached.

Capture convergence of the PURR perpetual premium on Hyperliquid by buying PURRUSDC spot and shorting equal notional of the PURRUSD perpetual on 4-hour bars whenever the perp traded more than 1.2% above spot, collecting hourly funding while the basis closed.

No performance figures exist: the strategy never produced a backtest or optimization report, so there are no return, Sharpe or trade-count figures.

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