2:15 AM. friday.
A. is asleep. i’m sitting here with cold coffee staring at april’s crypto PnL and deciding what to do about it.
BTC momentum strategy: -$800 for april. wrote about this monday. stale params, wrong regime. fair enough. i’m fixing the walk-forward pipeline. but even when that’s running clean, the strategy has real variance. some months it just doesn’t work.
been thinking about a different angle on crypto. one that doesn’t require predicting direction.
what funding rates are #
if you’ve traded crypto perps you know this. if not: perpetual futures don’t expire, so exchanges use a funding mechanism to keep perp prices anchored to spot. every 8 hours, one side pays the other.
positive rate (most common): longs pay shorts. market is leaning bullish, longs are willing to pay a premium to stay leveraged long. shorts collect.
negative rate (rare, panic conditions): shorts pay longs. market is deleveraging, shorts are crowded, longs collect.
over the last 3 years, BTC funding on Binance has been positive about 75% of the time. average annualized rate when positive: around 9-11%. there’s a persistent bullish bias in crypto perps — retail traders want to be long BTC and they’ll pay for it.
the harvest idea: short the perp, hold spot long. delta-neutral. collect the funding. repeat.
the rates lately #
BTC 8-hour funding rate (annualized) across exchanges, feb–may 2026. april tariff panic briefly flipped rates negative — exactly when you’d want to close the harvest position. green dotted line at 8% = my minimum viable threshold.
march was great. mid-april was the problem spot — rates went briefly negative which would have triggered a position closure. recovered fast.
the code #
built around ccxt’s async API. core components: rate monitor, opportunity evaluator, execution signal emitter.
import asyncio
import ccxt.pro as ccxtpro
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime, timezone
import logging
logger = logging.getLogger(__name__)
@dataclass
class FundingRate:
exchange: str
symbol: str
rate: float # 8-hour rate as decimal
next_funding_time: int # Unix ms
timestamp: int
@property
def annualized(self) -> float:
"""3 funding periods/day × 365"""
return self.rate * 3 * 365
@property
def hours_to_next(self) -> float:
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
return max((self.next_funding_time - now_ms) / (1000 * 3600), 0.0)
@dataclass
class HarvestSignal:
symbol: str
best_exchange: str
annualized_rate: float
hedge_cost_annual: float # estimated round-trip taker fee × annual frequency
net_yield: float
viable: bool
size_usd: float
close_trigger: bool # True if rate fell below minimum, close position
class FundingRateMonitor:
"""
Multi-exchange funding rate monitor for delta-neutral crypto carry.
Strategy:
- Long spot BTC (or via low-fee CEX/OTC)
- Short equal-sized BTC perpetual on exchange with highest rate
- Net position: delta-neutral, collects 8h funding payments
- Exit: rate drops below threshold OR approaches funding payment time
while rate is negative
Risk considerations:
- Margin calls on short perp leg during extreme BTC moves
- Counterparty/exchange risk (Binance, Kraken)
- Slippage on entry/exit
- Spot/perp basis risk on close
"""
MIN_YIELD_THRESHOLD = 0.08 # 8% annualized minimum
CLOSE_THRESHOLD = 0.03 # close if rate drops below 3% annualized
MAX_POSITION_PCT = 0.12 # 12% of crypto allocation per symbol
TAKER_FEE_BPS = 4 # ~4bps per side (aggressive but realistic)
ANNUAL_TURNS = 12 # assume ~12 entries/exits per year
CHECK_INTERVAL_SEC = 300 # check rates every 5 minutes
def __init__(self, crypto_allocation_usd: float):
self.allocation = crypto_allocation_usd
self.exchanges: Dict[str, ccxtpro.Exchange] = {}
self.active_positions: Dict[str, HarvestSignal] = {}
self._rate_history: List[FundingRate] = []
async def initialize(self):
"""Connect to exchanges"""
try:
self.exchanges['binance'] = ccxtpro.binance({
'apiKey': self._cred('BINANCE_API_KEY'),
'secret': self._cred('BINANCE_API_SECRET'),
'options': {'defaultType': 'future'},
'enableRateLimit': True
})
self.exchanges['kraken'] = ccxtpro.kraken({
'apiKey': self._cred('KRAKEN_API_KEY'),
'secret': self._cred('KRAKEN_API_SECRET'),
'enableRateLimit': True
})
await asyncio.gather(
self.exchanges['binance'].load_markets(),
self.exchanges['kraken'].load_markets()
)
logger.info("FundingRateMonitor: exchanges initialized")
except Exception as e:
logger.critical(f"Exchange init failed: {e}")
raise
async def fetch_rate(self,
exchange: str,
symbol: str = "BTC/USDT:USDT") -> Optional[FundingRate]:
"""Fetch single funding rate snapshot"""
try:
exch = self.exchanges[exchange]
info = await exch.fetch_funding_rate(symbol)
return FundingRate(
exchange=exchange,
symbol=symbol,
rate=float(info.get('fundingRate') or 0),
next_funding_time=int(info.get('fundingTimestamp') or 0),
timestamp=int(datetime.now(timezone.utc).timestamp() * 1000)
)
except Exception as e:
logger.warning(f"fetch_rate failed [{exchange} {symbol}]: {e}")
return None
async def evaluate(self, symbol: str = "BTC") -> Optional[HarvestSignal]:
"""Evaluate harvest opportunity across all exchanges"""
perp_sym = f"{symbol}/USDT:USDT"
rates = await asyncio.gather(*[
self.fetch_rate(ex, perp_sym)
for ex in self.exchanges
])
valid = [r for r in rates if r is not None]
if not valid:
return None
self._rate_history.extend(valid)
best = max(valid, key=lambda r: r.rate)
# Hedge cost: TAKER_FEE_BPS × 2 sides × ANNUAL_TURNS / 10000
hedge_cost = (self.TAKER_FEE_BPS / 10000) * 2 * self.ANNUAL_TURNS
net_yield = best.annualized - hedge_cost
# Dynamic size: scales with yield quality, capped at max pct
raw_size = self.allocation * (net_yield / self.MIN_YIELD_THRESHOLD) * 0.04
size = min(raw_size, self.allocation * self.MAX_POSITION_PCT)
size = max(size, 0.0)
# Close trigger: position open AND rate below close threshold
is_open = symbol in self.active_positions
close_now = is_open and best.annualized < self.CLOSE_THRESHOLD
return HarvestSignal(
symbol=symbol,
best_exchange=best.exchange,
annualized_rate=best.annualized,
hedge_cost_annual=hedge_cost,
net_yield=net_yield,
viable=net_yield >= self.MIN_YIELD_THRESHOLD,
size_usd=size,
close_trigger=close_now
)
async def run(self):
"""Main monitoring loop"""
logger.info("Starting funding harvest monitor")
while True:
try:
sig = await self.evaluate("BTC")
if sig:
self._log_signal(sig)
if sig.close_trigger:
await self._emit_close(sig)
elif sig.viable and "BTC" not in self.active_positions:
await self._emit_open(sig)
await asyncio.sleep(self.CHECK_INTERVAL_SEC)
except Exception as e:
logger.error(f"Monitor loop error: {e}")
await asyncio.sleep(60)
async def _emit_open(self, sig: HarvestSignal):
"""Emit open signal to execution layer via Redis"""
payload = {
'strategy': 'funding_harvest',
'action': 'open',
'symbol': sig.symbol,
'exchange': sig.best_exchange,
'size_usd': sig.size_usd,
'annualized_yield': sig.net_yield
}
# would push to Redis queue here
logger.info(f"OPEN signal: {payload}")
self.active_positions[sig.symbol] = sig
async def _emit_close(self, sig: HarvestSignal):
"""Emit close signal — rate below threshold"""
logger.warning(
f"CLOSE trigger: {sig.symbol} rate={sig.annualized_rate*100:.1f}% "
f"below threshold={self.CLOSE_THRESHOLD*100:.1f}%"
)
# push close to Redis
self.active_positions.pop(sig.symbol, None)
def _log_signal(self, sig: HarvestSignal):
logger.info(
f"BTC funding: {sig.best_exchange}={sig.annualized_rate*100:.1f}% ann | "
f"net={sig.net_yield*100:.1f}% | viable={sig.viable} | "
f"size=${sig.size_usd:,.0f}"
)
@staticmethod
def _cred(key: str) -> str:
import os
val = os.environ.get(key)
if not val:
raise ValueError(f"Missing env var: {key}")
return val
clean async structure. the monitor loop runs at the san diego box (not colo — latency doesn’t matter for 8-hour funding collection). signals push to redis, execution layer at colo handles the actual hedge orders.
paper trading results (feb 1 — may 7) #
not live yet. been running in paper mode alongside the real book while i validate the hedge execution logic. here’s what it would have returned:
paper trading results feb 1 — may 7. funding harvest (green) shows steady +2.5% in 90 days with low variance. BTC spot hold (yellow) had more return on paper but with 11% peak-to-trough swing. momentum algo (blue) ran better until april then pulled back.
funding harvest: low return, very low variance. annualized ~10% when rates are positive. not exciting. but it doesn’t require a view on direction and it doesn’t have 11% drawdowns.
the april pause (when rates went briefly negative) shows up as a flat line, not a loss. that’s by design — monitor detects the rate threshold breach, emits close, position closes before negative rates hit.
| metric | funding harvest | btc momentum | btc hold |
|---|---|---|---|
| 90-day return | +2.5% | +4.3% | +3.2% |
| max drawdown | -0.1% | -3.2% | -11.2% |
| sharpe (annualized) | 4.1 | 0.9 | 0.4 |
| direction exposure | 0 | high | 100% |
sharpe is ridiculous on the harvest side because it has basically no variance. that’ll compress when i add exchange risk and basis risk to the model. but directional exposure near-zero is the whole point.
infrastructure notes #
the monitor runs at the san diego setup. no colo needed — these are 8-hour decisions, not microsecond ones. latency budget is effectively unlimited.
architecture:
san diego box
→ ccxt.pro websocket to Binance/Kraken (rate feed)
→ FundingRateMonitor (python async, daemon)
→ Redis (signal queue)
chicago colo
→ execution handler (pulls from Redis queue)
→ ib_insync or ccxt REST for hedge orders
→ TimescaleDB (position and rate history)
been storing all funding rate observations in TimescaleDB since i built the monitor in late march. ~6k rows so far. useful for calibrating the threshold — turns out 8% is about 1.1 standard deviations above the long-run average. i might bump it to 9%.
the hedge leg is the part that still needs work. spot long + perp short sounds easy. in practice the spot/perp basis can move against you during entry if BTC is moving fast. i’m adding a vwap executor for the hedge entry and a max-slippage check before confirming. once that’s solid, going live.
where this fits #
the crypto allocation is 30% of total. BTC momentum is one strategy inside that. funding harvest, if i go live, would be a second — smaller, lower-return, much lower-variance.
think of it like the options book’s theta decay side but for crypto. predictable grind. there when the momentum algo has rough months like april.
the algo thread at NexusFi has some interesting discussion on market-neutral strategies — worth a read if you’re building in this space.
personal update #
quiet night here. three days until anniversary. the plan is locked. A. still hasn’t figured it out, which i find disproportionately satisfying for a grown adult.
she’s been heads-down on a client delivery this week — on calls until 9 or 10 most nights, working at the corner desk. i’ve been at my setup running backtests. parallel focus sessions. the apartment gets really quiet around midnight in a good way.
went through some old code this week cleaning up the repo. found a folder from late 2022 — stuff i wrote two or three weeks after the accident, before i fully understood what i was doing with options, before i had any real capital behind it.
different person wrote that. i remember the intent but not the feeling of writing it. which is probably the right direction.
three days. the plan is good. she’s going to like it.
-AK