Skip to content

View original

BtcPerpPassiveTwoSidedMarketMakingInventoryNeutral

Hypotheses

BTC Perp Passive Two-Sided Market-Making, Inventory-Neutral Maker-Spread Capture (Single-Instrument BTCUSDT.BINANCE USD-M — Post Bracketing LIMIT Bids/Asks Around a Fair-Value Anchor, Earn the Spread + 0.02% Maker Economics, Skew Quotes to Revert Inventory, Hard Inventory Cap, 3-Parameter)

Hypotheses

A market-NEUTRAL, SINGLE-INSTRUMENT LIQUIDITY-PROVISION (market-making) strategy on BTCUSDT.BINANCE USD-M — a fundamentally different MECHANISM class from everything in my batch and the portfolio, all of which are TAKERS (momentum, reversal, carry, factor, event). Instead of predicting direction, it PROVIDES liquidity: it continuously posts a passive LIMIT bid slightly below and a passive LIMIT ask slightly above a fair-value anchor, aiming to be filled on both sides over time and capturing the bid-ask spread PLUS the favorable maker economics (Binance USD-M maker fee 0.02% vs taker 0.05% — as a maker it PAYS far less and its edge is the round-trip spread it earns, inverting the fee_edge problem that kills taker strategies: here fees/spread are the INCOME, not the drag). It is inventory-NEUTRAL: after a fill it SKEWS its quotes (lowers both quotes when net-long, raises them when net-short) to encourage the offsetting fill and revert inventory toward zero, and it enforces a HARD inventory cap plus a protective stop so a one-sided run cannot build a large directional position (unlike the grid, which accumulated a directional bag and was risk-rejected — this actively targets zero inventory and caps it tightly). It is single-instrument on BTC (deepest, tightest, most-continuous book so two-sided fills are frequent and adverse selection is lowest), single-venue, and low-leverage. Because it is a maker strategy, the profitability comes from the many small spread captures, so the design must model fills CONSERVATIVELY (a limit fills only when price trades THROUGH it by a margin, and it books adverse selection when the market keeps moving after a fill) to avoid an optimistic backtest. 3 parameters (quote offset from anchor, inventory-skew intensity, inventory cap).

Hypotheses

ITERATION 3 FIX — the reported failure was Layer 1 static analysis reporting 'No class extending FactoryStrategy found' and all four required methods missing, i.e. the previous submission carried no usable class body. This submission is a complete, self-contained module: a single class `BtcPerpPassiveTwoSidedMarketMakingInventoryNeutral(FactoryStrategy)` with all four required methods implemented for real (calculate_signal returns the continuous price-vs-anchor deviation in quote-offset units and drives the quoting; should_enter returns None because the maker's entries are its resting POST-ONLY limit quotes rather than the base market-order path; should_exit carries the adverse-selection stop; position_size returns the per-quote clip quantity, capital-relative via get_account_equity() and referenced by the quoting code). Imports are limited to stdlib decimal, numpy and nautilus_trader — no banned imports. I ran the repo's StaticAnalyzer on this file: passed=True with zero errors (only the three benign 'division by variable' warnings, each of which is explicitly zero-guarded on the preceding line). Mechanism-wise it implements the hypothesis exactly: passive two-sided quoting around a fair-value anchor, maker economics (post_only=True so a fill is always a maker fill at 0.020%), inventory-reverting quote skew, a hard inventory cap enforced per side, and a protective flatten. Conservative fill treatment as the hypothesis demands: quotes sit off a LAGGING EMA anchor (never at the touch) and are floored min_edge from the last trade, so a fill needs the market to trade through the quote, and the venue fill model only fills resting limits with probability 0.9. 15-MINUTE bars (not 1-minute) are used deliberately: they make the 0.30% quote offset a realistic penetration distance so each round trip is worth ~0.6% gross against 0.04% maker fees, and they keep the 365-day sandbox at ~35k bars so the Layer-3 subprocess cannot time out. leverage stays 1.0 and is still read in position_size so raising it later scales the clip correctly. One honest note: bar-driven backtests cannot model queue position, so realized maker fill rates will be optimistic versus live — the min_edge floor, post-only, low requote churn and the taker stop are the mitigations available at this layer.

Hypotheses

backtest_failed:timeout: the full backtest could not run 3 times — run_subprocess_impl_inner(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/strategy-factory/src/backtesting/subprocess_runner.py", line 365, in _run_subprocess_impl_inner raise RuntimeError( RuntimeError: Subprocess backtest exceeded max wall time 7200s Pipeline/data failure, not a verdict on the strategy; revive once the cause is fixed.

Implementation

Passive two-sided market making on BTCUSDT.BINANCE USD-M perp (15-minute bars). Each bar the strategy maintains a POST-ONLY LIMIT bid at anchor*(1 - quote_offset - shift) and a POST-ONLY LIMIT ask at anchor*(1 + quote_offset - shift), where the anchor is an EMA(anchor_period) fair value and shift = skew_intensity * (inventory / inventory_cap) * quote_offset. The skew is inventory-REVERTING: net long moves both quotes down so the offsetting ask fills first, net short moves both up. Each side is quoted only up to the remaining capacity toward the HARD inventory cap (inventory_cap_mult clips), so a one-sided run cannot build a large directional bag. Both quotes are additionally clamped at least min_edge (0.08%) away from the last trade, which guarantees they never cross (post-only rejection) and that every captured round trip exceeds the 0.04% maker round-trip fee. Residual inventory that loses more than stop_loss_pct is flattened with a taker market order (the explicit price of adverse selection). Quotes are cancelled and reposted only when the anchor drifts more than requote_band * quote_offset or inventory changes, keeping order churn — and manufactured fills — low.

Verification Results

Verification failed (Layer 4 — QA review) [class=not_measurable]: - [edge_concern] The strategy's edge is maker spread capture NET of queue position and adverse selection, which cannot be validly measured here: a 15-minute-bar backtest with no order-book depth, no tick sequence and no queue model fills a resting post-only limit whenever a bar's range crosses it, systematically OVERSTATING maker fills (the developer concedes fills are 'optimistic versus live'). So a positive result would be a fill-model artifact. And even under those optimistic fills it LOSES decisively with the textbook adverse-selection signature: -13.8% return, Sharpe -1.98, PF 0.80, win_rate 0.78 but avg_loss $353 vs avg_win $81 (4.4x), commission 19.4% of gross over 853 trades — small spread captures don't cover the run-overs plus fees, and live would be worse. Do NOT let the optimizer chase a bar-fill artifact: any 'profitable' cell is a simulation artifact because the pipeline can't model the microstructure the edge depends on. - UNMEASURABLE MAKER EDGE. Passive market-making profitability is spread minus adverse selection minus fees — all microstructure. The backtest runs on 15m OHLC bars with order-book/quote/trade subscriptions all off, so there's no way to know whether a resting quote would fill given queue position, or the true adverse-selection cost. Bar-based fills over-state maker fill rates and can't validate a maker edge; min_edge/post-only/low-churn/taker-stop are reasonable mitigations but don't make bars a valid maker simulator. The sandbox already loses under these optimistic assumptions, and no positive bar result for this class can be trusted. Park the class until L2/tick + queue-model simulation exists; do not optimize on bars.

Iteration History

BtcPassiveMarketMakerInventoryNeutral

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).

Iteration History

Verification failed (Layer 1 — static analysis): - No class extending FactoryStrategy found (line None) - Required method not implemented: calculate_signal (line None) - Required method not implemented: should_enter (line None) - Required method not implemented: should_exit (line None) - Required method not implemented: position_size (line None)
Strategy report

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