Skip to content

View translation

SolWhaleVsRetailTakerFlowDivergenceLS15m

Hypotheses

SOL Whale-vs-Retail Taker Flow Divergence, Long-Short (BINANCE USD-M Perp, TRADE-TICK Driven, 15m Decision Bars, Trade the Side Large Aggressors Take When Small Aggressors Take the Other Side, Fixed-Notional Size Split, Low-Parameter)

Hypotheses

A LONG-SHORT, single-instrument MICROSTRUCTURE strategy on SOLUSDT.BINANCE driven by TRADE TICKS, not bars. The signal stratifies aggressive taker flow by TRADE SIZE and trades the DIVERGENCE between the two cohorts: when LARGE aggressors (single trades above a fixed USD notional — institutional/whale flow) are net BUYING while SMALL aggressors (retail) are net SELLING, go LONG; when large aggressors are net SELLING into retail buying, go SHORT. This fills the two most violated quota dimensions: short_1m_15m horizon (4.7% vs a >=10% target) and non-long-only direction (long_only is 84.1% vs a <=55% cap), and it introduces a DATA PRIMITIVE the portfolio barely uses — raw trade ticks with size stratification. The venue is Binance despite its over-weight (74.1%), and this is a deliberate, justified exception: the strategy needs multi-year SUB-HOURLY tick history, which Hyperliquid structurally cannot provide (its candle API serves ~5000 candles per interval, giving 15m only ~7 weeks), so routing this to an under-weight venue would guarantee an insufficient-history abandonment. SOL is chosen over BTC/ETH for fee headroom: its typical 1-3 hour move is roughly 2x a major's, so the ~0.10% round-trip futures cost consumes a far smaller share of the target move. Design is deliberately minimal (4 knobs) and, critically, uses a FIXED USD notional threshold for the large/small split rather than a rolling size percentile — a percentile requires re-sorting a trade-size buffer on every bar, which is precisely the O(n)-per-bar pattern that produced five consecutive Layer-3 300s timeouts.

Hypotheses

The zero-trade result was a DATA-WINDOW failure, not a threshold failure, and I verified both halves of that claim before changing anything. (1) The sandbox ran on 1-MINUTE bars, whose ~20k-bar budget is a ~28-day window ending at the newest bars, while the collected SOLUSDT.BINANCE trade-tick history in the catalog spans 2026-04-29..2026-07-17 — the two do not intersect, so on_trade_tick never fired, both cohort totals stayed 0, and the divergence was a constant 0.0 that no threshold could trigger. (2) I replayed the exact cohort logic over all 8.54M collected SOL ticks: 3,331 flow-bearing 15m bars, large_imb spanning ±0.28, small_imb ±0.18, divergence p5/p95 = -0.224/+0.195, and the CURRENT gate (entry_thresh 0.18 plus the strict opposite-sign retail test) fires on 430 of those bars — 12.9%. So the entry logic is not too tight and I deliberately did NOT loosen it, which would only add marginal-divergence trades that cannot clear the ~0.10% round trip. The fix is the decision bar: bar_type moves from 1-MINUTE to 15-MINUTE, which is what the hypothesis specifies in its own title and whose ~208-day sandbox window fully contains the tick history. Code changes are confined to documentation of the measured signal scale (the earlier ±0.05 retail estimate was wrong) plus counters and an on_stop diagnostic line reporting ticks/flow_bars/entry_fires, so a future zero-trade report can distinguish missing flow from a gate that never fired instead of leaving the two indistinguishable. All signal, entry, exit and sizing logic is otherwise byte-identical, and _param_bounds now mirrors the searched knobs.

Hypotheses

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

Long-short trade-tick microstructure strategy on SOLUSDT.BINANCE USD-M perp with 15-minute decision bars. Every aggressive taker trade is classified by a FIXED USD notional (large_usd) into whale vs retail flow, and each cohort's signed notional imbalance is accumulated over a trailing window of flow-bearing 15m bars. The traded signal is the divergence large_imbalance - small_imbalance: go LONG only when whales are net buying WHILE retail is net selling and the divergence clears entry_thresh, SHORT only in the mirror case. Exits are a hard 1.2% stop, a 0.8% take-profit (several multiples of the ~0.10% round-trip taker cost), an adverse divergence flip, a 12-bar time stop, and a divergence-collapse exit gated on the trade having cleared 0.15%. Work is O(1) per tick and O(lookback) per bar, with no size percentile and no history rescan; with no tick flow in the window both imbalances are 0 and the strategy stands down rather than substituting a price proxy.

Verification Results

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

Iteration History

Verification failed (Layer 3 — sandbox backtest): No trades produced Bar type used: SOLUSDT.BINANCE-15-MINUTE-LAST-EXTERNAL, Bars processed: 204740 Diagnostics: should_enter() returned a side 0 times over 204732 evaluated bars -> your ENTRY CONDITION never triggered. Loosen the entry logic / thresholds. Ensure your strategy produces trades with the given data and parameters.

Iteration History

Verification failed (Layer 4 — QA review): - The hypothesis's named entry condition is not implemented. Title and description both state the trigger explicitly: 'Trade the Side Large Aggressors Take WHEN SMALL AGGRESSORS TAKE THE OTHER SIDE' / 'when LARGE aggressors are net BUYING while SMALL aggressors are net SELLING, go LONG'. Iteration 2 removed the opposite-sign test (large_imb > 0 > small_imb) and replaced it with (large_imb > 0) AND (divergence >= 0.15). These are not equivalent: large_imb=0.50, small_imb=+0.30 gives divergence=0.20 and fires a LONG even though retail is net BUYING alongside the whales — the exact configuration the hypothesis excludes. The strategy now trades whale-imbalance-with-a-relative-tilt, not whale-vs-retail divergence, so the stated edge is not the edge being traded. Note the sign asymmetry this produced: 135 shorts vs 88 longs. - Fee-unviable exit structure. There is NO take-profit and no minimum-move target of any kind — the only exits are a 1.5% hard stop, a divergence collapse (|signal| < 0.05), and a divergence flip. Since entry needs |divergence| >= 0.15 and exit fires at |divergence| < 0.05, positions close on flow-noise decay after ~1h23m average hold with no requirement that price moved far enough to clear the ~0.10% round-trip taker cost. The sandbox confirms the structure is the problem: total_commission 19,362 USD = 94.83% of gross PnL, turnover 2788%, avg_trade_return_pct -0.0536% against the 0.15% futures floor. - The hypothesis's central data premise is unmet, so no downstream result is trustworthy. The hypothesis routes to Binance specifically because the strategy 'needs multi-year SUB-HOURLY tick history'. The developer's own rationale documents that SOLUSDT.BINANCE trade-tick coverage is ~30 days (32 parquet files, 2026-06-18..2026-07-17) against ~204,740 bars (~5.8y) — ~98.6% of bars have zero tick flow and identically-zero divergence. The sandbox ran on data_days=35 with 223 trades. Optimization and the overfit/holdout gates on a 35-day, 223-trade microstructure sample will produce noise, and a full-history backtest would show 5.8 years of flat equity around a 35-day active window, distorting every metric the analyst reads. - Leverage is read via getattr(self.config, 'leverage', 1.0). This is the exact pattern the leverage_set_but_unused gate rejected on a prior strategy (AvaxVolatilitySpikeWickRejectionFadeLS15m iteration 1) — the static analyzer does not recognize getattr access as leverage usage. Layer-1 passed here, but that gate runs after Layer 3 and will likely block this strategy for the same reason. - Sizing is aggressive relative to the stop: capital_frac 0.5 x leverage 2.0 = 100% of equity in notional per trade (sandbox avg_position_pct 126%), so the 1.5% hard stop costs ~3% of equity per stopped trade. With max_consecutive_losses=14 observed, this compounds fast (max_drawdown 59%). - During warmup, if the base template does not call calculate_signal before min_bars_required, ticks accumulate into self._cur without ever being committed or reset, so the first committed bucket may aggregate an arbitrarily long span of flow. Separately, the deques are appended once per decision bar regardless of whether tick data existed, so zero-flow bars silently dilute the trailing window (both imbalances land at exactly 0.0).

Iteration History

Verification failed (Layer 3 — sandbox backtest): No trades produced Bar type used: SOLUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL, Bars processed: 40747 Diagnostics: should_enter() returned a side 0 times over 40687 evaluated bars -> your ENTRY CONDITION never triggered. Loosen the entry logic / thresholds. Ensure your strategy produces trades with the given data and parameters.
Strategy report

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