I built a delta-neutral crypto arbitrage bot
| About | Research | Resume | Startup Adventures | Blog |
May 2026 · Back to blog
I wanted a quant project I could build fast and actually understand end to end. Crypto funding rate arbitrage fits that pretty well. The strategy is simple in concept and has real math behind it, so it's a good thing to build from scratch.
How it works
Binance perpetual futures have a funding mechanism to keep the price tethered to spot. Every 8 hours, if the perpetual is trading at a premium to spot, longs pay shorts. If it's at a discount, shorts pay longs. The rate is published ahead of each settlement.
The strategy: short the perpetual, long the same amount of spot. Your price exposure is approximately zero since you own the asset on spot and are short the same notional in futures. What you collect is the funding payment every 8 hours, for as long as the rate stays elevated. When the rate normalises, you close both legs.
The hard part is knowing when to enter and when to stay out.
The signal design
A high rate right now is not a good enough reason to enter. Rates can spike for a few hours and revert before you've collected enough funding to cover fees. At $1,000 position size, the round-trip fee is $2.70. At a 30% annualised rate, it takes about 3.3 days of sustained elevation to break even.
So the signal has a persistence filter. It looks at the last 12 settlement ticks (4 days worth) and only enters if 75%+ of them were above the threshold. That means the rate needs to have been consistently elevated, not just spiking now. This single filter eliminated 96% of potential entries and was the difference between the strategy working and not working.
There's also a z-score check to block entry during statistical outlier spikes, and a fee viability check to confirm positive expected value before any order goes in.
Backtest results
Oct 2023 - Apr 2024 was a bull market period where funding rates averaged 20-25% annualised across BTC, ETH, SOL, and DOGE. Good conditions for this strategy.
Results on $10,000 capital: Sharpe 3.21, max drawdown 0.089%, 21 trades, 14 wins. Total funding collected $112.73, fees paid $56.70, net PnL $59.03. The four best trades (SOL and ETH positions held for 168-232 hours in February and March 2024) accounted for 69% of total profit.
The pattern is what you'd expect: shorter holds lose money because fees outrun funding. Longer holds in sustained high-rate environments is where the strategy actually makes money. The persistence filter is what selects for those conditions.
Right now
The strategy is currently dormant. Nov 2025 - May 2026 has been a low-rate regime. Average annualised rates are 1.2-1.8% across most pairs, and the maximum observed rate hasn't broken 11% (which is basically the Binance floor rate). No qualifying conditions, no trades, zero drawdown.
That's the correct behavior. The strategy is designed to sit out markets where it can't make money. The alternative is forcing trades to stay busy, which just means paying fees for nothing.
Three bugs I found during backtesting
Before I got to clean results, I hit three bugs that would have produced completely wrong backtest numbers.
The first was live WebSocket data contaminating historical records. The stream updates every second during bot operation. Each update was being written to the database with a current timestamp, indistinguishable from actual 8-hourly settlement records. The normaliser saw these as 82 consecutive elevated ticks and generated signals on data that was just live noise. Fix: filter queries to rows where the timestamp is exactly at 00:00, 08:00, or 16:00 UTC.
The second was a warm-up artefact in the persistence scorer. When it had seen only one observation and that observation was above threshold, it computed 1/1 = 100% persistence and fired an entry signal immediately. Fix: return 0 until the full window is populated.
The third was the backtest engine ignoring the injected normaliser's window size and hardcoding its own. With window_size=24 instead of 12, the longest sustained cluster in the 2025-2026 data (11 consecutive ticks) only scored 45.8% persistence, never hitting the 75% threshold. Zero trades generated despite real opportunities existing. Fix: read window size from the injected normaliser instead of recalculating it.
All three would have been invisible without careful data auditing before trusting any results. I wrote a proper data validation layer first for exactly this reason.