DEV Community

KX
KX

Posted on • Originally published at godzilla.dev

That 300% funding APR is not free money: screening for squeeze traps on Binance perpetuals

In late December 2023, TRB went from around $200 to over $600 in a single session on Binance, then collapsed just as fast. Hundreds of millions in short positions were liquidated on the way up. In the days before the move, TRB's perpetual had been flashing exactly the kind of numbers that make a funding rate arbitrageur's eyes light up: deeply skewed funding, fat annualized carry, seemingly free money for anyone willing to take the other side.

Anyone who took that carry trade got carried out.

This post is about the screening layer I run before any funding rate position goes on: a Python script that scores every USDT perpetual on Binance against five structural risk signals, using only public endpoints. No API keys, no paid data. The full script is at the end; the interesting part is why each signal works.

The trap, mechanically

The standard funding rate arbitrage is delta-neutral on paper. Funding is printing high positive? Short the perp, buy spot, collect the payments. Deeply negative? Long the perp, hedge with a spot short. Price risk cancels, funding accrues. In liquid majors this is a boring, capacity-constrained, mostly honest trade.

In a low-float token it can be bait.

Here is the failure mode. A token has a small circulating supply, and a large share of that float sits with a few wallets. Extreme funding appears — often because positioning is already crowded on one side. Carry traders pile in, shorting the perp against spot. Then the float holders push the spot price, hard. Three things break at once:

  1. The perp leg gets margin-called before the hedge helps you. Your spot leg is profitable, but it's sitting in a wallet; your perp short is sitting on an exchange with leverage, being marked against a price that someone else controls. Liquidation doesn't wait for your rebalancing script.
  2. The basis blows out. Perp and spot are supposed to converge via funding. During a squeeze the perp can trade at absurd premiums for hours. "Delta-neutral" assumes the two legs move together; in the tail they don't.
  3. The exit is a door one person wide. Thin spot books mean unwinding the hedge leg costs you a chunk of the carry you came for — if you can unwind at all.

The nasty part: the trade looks most attractive exactly when it is most dangerous. Extreme funding is both the lure and the symptom. So the job of a screener is not to find high funding — that's one API call — but to answer a different question: is this a market where the other side can hurt me on purpose?

Five signals

The screener scores each perpetual on five structural signals. Each one has a "warning" and a "danger" threshold; warning adds 1 point, danger adds 2. The thresholds come from going back over historical squeeze events and asking what the market looked like before the candle.

1. Open interest vs. circulating market cap

oi_mcap_ratio = open_interest_notional / circulating_market_cap
Enter fullscreen mode Exit fullscreen mode

This is the single most informative number. If the notional value of open perpetual contracts exceeds half the circulating market cap of the underlying token, the derivative tail is wagging the spot dog. Above 1.0 — more paper exposure than actual float value — a determined actor doesn't need to fight the market to move the mark price; the market is smaller than the bet on it.

Warning above 0.5, danger above 1.0. For reference, majors like BTC and ETH sit far below these levels; the tokens that end up in squeeze post-mortems almost always screened hot here first.

2. Perp-to-spot volume ratio

perp_spot_ratio = perp_volume_24h / spot_volume_24h
Enter fullscreen mode Exit fullscreen mode

Healthy markets discover price on spot and lever it on derivatives. When 24h perp volume runs 15–40x spot volume, price discovery has effectively moved to the perp, and the spot print — the thing your hedge depends on, and often the input to the mark price — is thin enough to be pushed cheaply. Warning above 15, danger above 40.

There's an important edge case here, covered below: tokens whose perp trades on Binance but whose spot doesn't.

3. Funding rate extremity

funding_apr = last_funding_rate * 3 * 365   # 8h funding, annualized
Enter fullscreen mode Exit fullscreen mode

Annualized funding above 100% is a warning; above 300% is danger territory. Not because the carry isn't real — it is, for as long as it lasts — but because triple-digit APRs don't survive in efficient markets. If the number looks like a DeFi farm from 2021, someone is being paid that much to hold a position nobody sane wants, and you should ask why the other side is this desperate.

Note the abs(): deeply negative funding is scored the same as deeply positive. Squeezes come in both directions.

4. Listing age

Contracts live for under 60 days score danger; under 180, warning. New listings combine every risk factor: no funding history to judge what "normal" looks like, concentrated early float, immature spot liquidity, and maximum attention from exactly the kind of trader who runs squeezes. A disproportionate share of historical trap events happened within the first two quarters of a perp's life.

5. Circulating market cap, absolute

Below $100M warning, below $30M danger. Small caps aren't automatically manipulated, but manipulation is a fixed-cost business — the smaller the float, the cheaper the squeeze. This signal overlaps with signal 1 by construction, and that's intentional: a token that trips both is small and over-levered, which is the full trap setup.

Bonus signal: no market cap data at all

If a token's perp trades on Binance but the token doesn't rank in CoinGecko's top ~2000 by market cap, the screener can't compute signals 1 and 5 — and that absence is itself worth a point. A perpetual contract on an asset too small or too new to have reliable supply data is not where a delta-neutral strategy goes to earn a quiet carry.

The unglamorous parts (where the bugs live)

Three implementation details caused more trouble than the actual scoring logic.

Symbol collision on CoinGecko. Matching Binance base assets to CoinGecko entries by ticker symbol is a minefield — ticker symbols aren't unique, and a $20M token can share a symbol with a $2B one. The screener pulls CoinGecko's markets endpoint ordered by market cap descending and keeps the first (largest) match per symbol:

for c in data:
    sym = (c.get("symbol") or "").lower()
    if sym in wanted and c.get("market_cap"):
        # on symbol collision, keep the highest-mcap match
        mcap.setdefault(sym, float(c["market_cap"]))
Enter fullscreen mode Exit fullscreen mode

This biases toward under-flagging (you might attribute a big token's mcap to a small impostor and miss a trap), which is the conservative direction for a screener whose job is to justify a "no" — but know the limitation.

The 1000x prefix. Binance lists some low-price tokens as 1000PEPE, 1000SHIB etc. — the contract multiplies the price by 1000. For market cap lookups the prefix has to be stripped:

cg_base = base[4:] if base.startswith("1000") else base
Enter fullscreen mode Exit fullscreen mode

Miss this and every 1000-prefixed contract silently gets zero market cap and a spurious flag.

Orphan perps. Some perpetuals trade on Binance futures with no corresponding Binance spot pair at all. For these, the perp/spot ratio is set to infinity rather than skipped:

if r.spot_vol > 0:
    r.perp_spot_ratio = r.perp_vol / r.spot_vol
elif r.perp_vol > 0:
    r.perp_spot_ratio = float("inf")  # perp exists, spot doesn't
Enter fullscreen mode Exit fullscreen mode

A perp whose hedge leg would have to live on a different exchange is a materially worse trade — cross-exchange transfer time is exactly the window in which a squeeze kills you. Infinity, not N/A.

Scoring and output

The scoring function is deliberately dumb — transparent beats clever in a risk filter:

def score_row(r: Row) -> None:
    def add(cond_hi, cond_mid, name):
        if cond_hi:
            r.score += 2
            r.flags.append(f"{name}!!")
        elif cond_mid:
            r.score += 1
            r.flags.append(name)

    add(r.oi_mcap_ratio > 1.0, r.oi_mcap_ratio > 0.5, "OI/MCAP")
    add(r.perp_spot_ratio > 40, r.perp_spot_ratio > 15, "PERP/SPOT")
    add(abs(r.funding_apr) > 3.0, abs(r.funding_apr) > 1.0, "EXTREME_FUNDING")
    add(0 < r.listing_days < 60, 60 <= r.listing_days < 180, "NEW_LISTING")
    add(0 < r.mcap < 3e7, 3e7 <= r.mcap < 1e8, "MICRO_CAP")
    if r.mcap == 0:
        r.score += 1
        r.flags.append("NO_MCAP_DATA")
Enter fullscreen mode Exit fullscreen mode

Maximum score is 11. In practice I treat the bands roughly as: 0–1 normal market, size the carry trade on its own merits; 2–3 proceed with reduced size and tighter liquidation buffers; 4+ the funding is not the opportunity, it's the advertisement. The point of a screener is to make "no" cheap.

Running it takes about a minute for the full universe (the per-symbol open interest endpoint is the bottleneck; a small thread pool keeps it tolerable, and CoinGecko's free tier wants a 1.2s pause between pages):

$ python trap_screener.py --min-score 3
Fetching contract list...
530 perpetuals, fetching tickers...
Fetching market caps (CoinGecko)...
Fetching open interest...

CONTRACT         RISK  MCAP($M)  OI/MCAP    P/S  FUND APR  AGE(d)  FLAGS
------------------------------------------------------------------------
SLXUSDT             6      45.8     0.18    inf     -113%      37  PERP/SPOT!!,EXTREME_FUNDING,NEW_LISTING!!,MICRO_CAP
DATAIPUSDT          6       0.0     0.00    inf     -104%       5  PERP/SPOT!!,EXTREME_FUNDING,NEW_LISTING!!,NO_MCAP_DATA
EVAAUSDT            6      20.4     1.82    inf       40%     277  OI/MCAP!!,PERP/SPOT!!,MICRO_CAP!!
STARUSDT            6      27.3     0.05    inf        5%      55  PERP/SPOT!!,NEW_LISTING!!,MICRO_CAP!!
CTRUSDT             6      12.9     0.08    inf        5%      40  PERP/SPOT!!,NEW_LISTING!!,MICRO_CAP!!
GWEIUSDT            5     202.4     0.06    inf     -314%     160  PERP/SPOT!!,EXTREME_FUNDING!!,NEW_LISTING
ARXUSDT             5      38.4     0.08    inf      -49%      15  PERP/SPOT!!,NEW_LISTING!!,MICRO_CAP
BIRBUSDT            5      17.0     0.10    inf       -8%     160  PERP/SPOT!!,NEW_LISTING,MICRO_CAP!!
...
Enter fullscreen mode Exit fullscreen mode

What this doesn't catch

Honesty section. The screener reads market structure; it cannot see intent. It won't catch a coordinated squeeze on a mid-cap with healthy-looking ratios, an exchange listing announcement that turns structure upside down in an hour, or unlock-schedule cliffs (that data lives elsewhere and is worth adding). It also scores a snapshot — a token can screen clean at noon and be a trap by dinner. This is a pre-trade filter, not a substitute for position-level risk management: liquidation buffers, basis monitoring, and an exit plan sized to actual spot depth.

The full script (~200 lines, requests is the only dependency) is here: https://github.com/godzilla-foundation/godzilla-community/blob/main/strategies/trap_screener/trap_screener.py. Run it, argue with the thresholds, send patches.


I maintain godzilla.dev, an open-source C++/Python framework for self-hosted funding rate arbitrage and market making. The screener in this post is the research side of the problem; execution — actually running the delta-neutral legs with microsecond-level latency without becoming the exit liquidity — is a different one.

Top comments (0)