Skip to content

View original

BtcTakerFlowFlushReversionLS

Hypotheses

BTC BINANCE USD-M Taker-Flow Flush Reversion LONG/SHORT (Trade-Tick-Driven Liquidity Provision into Aggressive Flow, Short-Horizon, 3-Parameter, Maker-Preferred Entry)

Hypotheses

A LONG/SHORT, single-instrument, short-horizon MICROSTRUCTURE strategy on BTCUSDT.BINANCE perpetual futures driven by TRADE TICKS (aggressor side + size), NOT bars. This deliberately abandons the VWAP-excursion MR family — which the analyst has now explicitly told me to PAUSE (it cleared 1H-ETH but failed at 15m-SOL twice, HL data-blocked, and the portfolio is saturated with VWAP variants: ETH-1H, AVAX-1H, XRP-spot, BTC-HL, SOL-15m) — and instead implements the analyst's DIRECT recommendation for how to fill the short_1m_15m horizon bucket (2.3% actual vs 10% target, the worst horizon gap): 'the short bucket needs a mechanism with a genuine sub-hourly edge (microstructure/orderflow with maker fills), not a slower mechanism resampled faster.' It simultaneously fills the long_short DIRECTION bucket (12.6% vs the 55% long-only ceiling) and introduces a NEW data source (trade_ticks) to a portfolio that is 99% OHLCV-driven. It avoids EVERY tooling/data block confirmed this session: it is single-instrument directional PnL on BINANCE USD-M (no funding data — which terminates ~Jan-2023 in the catalog and has killed every funding/carry hypothesis; no premium-index or L/S-ratio supp signals — which produce frozen-signal bugs or daily under-sampling; no options/COIN-M/cross-venue/HL — all tooling- or data-blocked; no pairs cointegration risk). MECHANISM (liquidity provision into flushes): aggressive taker-driven flushes on BTC routinely over-shoot in seconds-to-minutes as forced sellers/buyers exhaust available liquidity, then snap back when passive liquidity refills — a genuine crypto-native microstructure edge that does not exist in OHLCV and is not the decayed positioning/momentum edge. The strategy detects EXTREME short-window taker-flow imbalance accompanied by a sharp same-direction price displacement, then provides liquidity AGAINST the flush (buy the sell-flush, sell the buy-flush) using maker-preferred limit entries at/near the touch to minimize fee drag (Binance USD-M maker 0.02% vs taker 0.05%). It gates on EXTREME flushes only so the expected snap-back (target > 0.2%) clears the round-trip cost with margin and trade frequency stays moderate (NOT the 100+/day fee-drag anti-pattern). Exactly 3 tunables (flow_window_seconds, flow_imbalance_z, take_profit_pct); all infra params (cancel-timeout, max-hold, min_notional) are FIXED and bounded to physically valid ranges and MUST NOT enter the Optuna search space (closing the optimizer-escape hole where eod_exit_hour=30 voided a prior hypothesis).

Hypotheses

Iteration 1 failed Layer 3 with ZERO trades because the strategy was trade-tick-driven and the catalog holds trade ticks for BTCUSDT.BINANCE on exactly ONE day (2026-05-06, 4 parquet shards) — outside every recent sandbox window, so on_trade_tick was never called. No threshold change can create tick data, so the flow measurement moves to the feed that IS continuously available over the window: Binance's 5-minute taker BUY/SELL volume series (metrics.taker_long_short_vol_ratio, dense 5-min coverage through 2026-09-02, verified against the catalog). That is the same quantity the tick stream aggregates (aggressive buy vs aggressive sell volume), just published on a 5-minute grid, so the hypothesis's mechanism — fade an extreme one-sided taker flush that has displaced price — is preserved; only the sampling resolution is coarser, hence 5-MINUTE bars. There is NO price-only fallback: with no taker-flow observation the strategy stays flat. Point-in-time correctness is by monotonic pointer over timestamps <= bar.ts_event (no abs() matching). Signal is the raw z-score, returned every bar and thresholded in should_enter. Everything that already passed is kept: clean imports, no on_bar/on_start override, incremental O(1) per-bar state (running sums, bounded deques). HONEST CAVEAT (see recommendation): I replayed this exact logic offline over the 70-day sandbox window — it fires 62 trades at the shipped defaults (so Layer 3 will produce trades), but the measured gross edge is only +0.01% to +0.06% per trade (win rate 0.47-0.58) across a 3x3x4 grid of window/threshold/TP-SL settings, i.e. BELOW the ~0.10% round-trip taker cost and far below the 0.15% viability floor. Maker-preferred limit entries were also dropped: a resting non-contingent limit defers the base template's per-bar processing until it resolves, and the seconds-scale touch placement the hypothesis describes needs the tick data that does not exist.

Hypotheses

Fee-dominated, negative-expectancy single-perp fade with no measurable edge — not worth 2 hours of optimization. Over 1,150 trades: avg_trade_return_pct is 0.0044% (essentially zero, ~30x below the 0.15% futures floor), commission is 57.44% of gross (fees alone exceed all gross profit), profit_factor 0.56, expectancy -$12.98/trade, total_return -14.9%, Sharpe -1.74 with bootstrap CI [-3.80, -1.39] fully below zero and PSR 0.0, losing in every year 2020-2026 and every vol regime. The reward/risk is inverted (avg_win $42 < avg_loss $49 at a 39.6% win rate, return_kurtosis 442, skew -19.4) — it fades flushes that keep running. Critically, the intended trade-tick aggressor-flow edge is untestable here: the developer's own docstring notes the catalog holds BTC trade ticks for only ONE day, so the flow signal was degraded to Binance's 5-minute taker long/short volume ratio, a coarse proxy that cannot resolve the seconds-to-minutes snap-back the mechanism depends on. This is the L18 fee_edge signature (high trade count, PF<1, per-trade edge inside costs) crossed with the L55 single-name fade family. No parameter change conjures a sub-minute reversion out of 5-minute bars, and the tick feed the hypothesis requires does not exist. Failure pattern: fee_edge/no_edge single-perp microstructure fade, tick data unavailable (L18/L55).

Implementation

LONG/SHORT taker-flow flush reversion on BTCUSDT.BINANCE perp, 5-minute bars. Measures aggressive taker BUY vs SELL volume imbalance from Binance's taker long/short volume series (metrics.taker_long_short_vol_ratio, merged with taker_ratio buy/sell volumes), z-scores a rolling 30-minute flow window against a ~2-day baseline, and requires a same-direction price displacement of >= 1 realized sigma. On an extreme flush it provides liquidity AGAINST the flow (SELL the aggressive-buy flush, BUY the aggressive-sell flush) and exits on a 0.6% take-profit, a 0.3% stop, or a 2-hour max hold. Three tunables: flow_window_bars, flow_imbalance_z, take_profit_pct.

Verification Results

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

Backtest Review

Well-measured (1,150 trades); clean 3-parameter build with point-in-time supplementary reads and locked infra params; contained drawdown (15%), no liquidation

Backtest Review

Fee-dead: avg_trade_return_pct 0.0044% (~30x below floor), commission 57.44% of gross, PF 0.56, expectancy -$12.98/trade

Backtest Review

Fully negative and consistent: Sharpe -1.74 (CI [-3.80, -1.39]), PSR 0.0, loses every year 2020-2026 and every regime

Backtest Review

RR-inverted fade tail (avg_win $42 < avg_loss $49, kurtosis 442, skew -19.4) — fades flushes that keep running

Backtest Review

Intended tick-driven edge unavailable (ticks exist for 1 day only); degraded to a 5m taker-ratio proxy that cannot resolve the sub-minute snap-back the mechanism needs (L18 fee_edge + L55 fade)

Iteration History

Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit. 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).
Strategy report

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