Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 59 additions & 19 deletions benchmarks/perp-fees.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ slug: perp-fees
number: "007"
title: Cheapest perp DEX, live all-in fee on a $1000 ETH 10x long
seo_title: "Cheapest perp DEX 2026"
seo_description: "{{best_name}} leads cheapest perp DEX at {{best_p50}} all-in (24h avg). $1000 ETH 10x long. Lighter, Hyperliquid, dYdX v4, GMX v2, gains.trade ranked live."
subtitle: All-in cost in basis points to open a $1000 ETH long 10x position. Taker fee plus half-spread plus impact, measured live from public APIs across Lighter, Hyperliquid, dYdX, GMX and gains.trade.
seo_description: "{{best_name}} leads cheapest perp DEX at {{best_p50}} all-in (24h avg). $1000 ETH 10x long, plus $100k and $1M size panels. Lighter, Hyperliquid, Polymarket, dYdX v4, GMX v2, gains.trade ranked live."
subtitle: All-in cost in basis points to open a $1000 ETH long 10x position, with companion panels at $100k and $1M where book depth takes over from fees. Taker fee plus half-spread plus impact, measured live from public APIs across Lighter, Hyperliquid, Polymarket, dYdX, GMX and gains.trade.
category: Trading
status: live
metric: All-in cost
Expand All @@ -27,8 +27,12 @@ seo_intro: |
gains.trade are oracle priced, so their spread terms come from
protocol parameters), and sum the two into a single basis-point
figure refreshed every five minutes. Hyperliquid vs
Lighter vs dYdX vs GMX vs gains.trade is then a one-number
comparison, not a five-tab spreadsheet.
Lighter vs Polymarket vs dYdX vs GMX vs gains.trade is then a
one-number comparison, not a six-tab spreadsheet. Size changes the
answer, so the board is complemented by two panels that rerun the
exact same measurement at $100,000 and $1,000,000 of notional. Small
orders reward the lowest fee schedule; a $1M market order rewards the
deepest book, and the ranking visibly flips between the two.

abstract: |
We measure the live all-in cost of opening a $1000 ETH long 10x perp
Expand All @@ -52,15 +56,18 @@ methodology:
- "GMX v2 (Arbitrum): Subsquid GraphQL on the synthetics-arbitrum subgraph for `positionFeeFactorForNegativeImpact` (worst-case open) on the ETH/USD, BTC/USD and SOL/USD markets + gmxinfra REST for funding. Oracle priced, no orderbook: the spread term is zero."
- "Lighter: `/orderBookDetails` (taker fee per market) + `/orderBookOrders` (orderbook walk)."
- "gains.trade (Gains v8 on Base): fees read directly on-chain. `eth_call pairs(N)` gives `spreadP` and `feeIndex`, then `eth_call fees(feeIndex)` gives the open fee (`totalPositionSizeFeeP`). The taker crosses one side of the book, so we charge half of `spreadP`. SOL has `spreadP` 0 on-chain (Gains prices that pair's spread dynamically), so its figure is the open fee alone. Values in 1e10 precision per Gains v8 convention. Fee tier cached 1h, pair config 6h."
- "Paradex: `/markets` fee config for the api-tier taker rate + `/orderbook?depth=100` walk. The book endpoint caps at 100 levels (about $1M visible on majors), so the $1M tier can be skipped when the visible book thins."
- "Extended: full public book from `/info/markets/<m>/orderbook` + `/stats` funding (1h native). Its taker fee is not exposed by any public endpoint; the documented base rate of 2.5 bps is used and this is the one venue where the fee is not read live."
- "All-in formula: `all_in_bps = taker_fee_bps + spread_bps`. Both components emitted as separate metrics for transparency."
- "Notional tiers: the same measurement runs at $1,000, $10,000 and $100,000 by rewalking the already fetched book, published to `perp_fees_all_in_bps_tier{venue, chain, notional}`. The headline `perp_fees_all_in_bps` stays defined at $1,000. A tier the book cannot fill is skipped and counted in `perp_fees_tier_skipped_total`, never extrapolated. Oracle priced venues (GMX v2, gains.trade) charge a flat percentage of size, so their figure repeats across tiers."
- "Notional tiers: the same measurement runs at $1,000, $10,000, $100,000 and $1,000,000 by rewalking the already fetched book, published to `perp_fees_all_in_bps_tier{venue, chain, notional}`. The headline `perp_fees_all_in_bps` stays defined at $1,000. A tier the book cannot fill is skipped and counted in `perp_fees_tier_skipped_total`, never extrapolated. Oracle priced venues (GMX v2, gains.trade) charge a flat percentage of size, so their figure repeats across tiers."
- "Book depth for the $1M tier: Lighter is walked on its top 100 levels, dYdX on the full indexer book, Polymarket on up to 500 levels. Hyperliquid returns its 20 best raw levels; when those cannot absorb the tier the harness refetches with nSigFigs=5, the finest price aggregation the API offers, which extends coverage with negligible price rounding."
- "Failures (5xx, timeouts, rate limits) leave the previous gauge in place and increment a per-venue `fetch_errors_total` counter. The page falls back to the last successful sample."

per_chain_explainer:
- slug: ETH
h2: "Cheapest perp DEX for ETH"
body: |
The cheapest perp DEX for ETH on this benchmark sits at {{best_p50:chain:ETH}} all-in (p50, 24h) for a $1000 ETH long 10x. ETH-PERP is the deepest pair on every venue measured (Hyperliquid, Lighter, dYdX v4, GMX v2, gains.trade), which means the rack-rate taker fee dominates the all-in number rather than the spread. Lighter quotes 0 bps taker, Hyperliquid 4.5 bps, dYdX 5 bps tier-0, GMX 4 or 6 bps on the impact branch, gains.trade reads the openFeeP slot live from the on-chain Gains v8 fees contract. Spread plus impact at $1000 notional is added on top via an orderbook walk.
The cheapest perp DEX for ETH on this benchmark sits at {{best_p50:chain:ETH}} all-in (p50, 24h) for a $1000 ETH long 10x. ETH-PERP is the deepest pair on every venue measured (Hyperliquid, Lighter, Polymarket, dYdX v4, GMX v2, gains.trade), which means the rack-rate taker fee dominates the all-in number rather than the spread. Lighter quotes 0 bps taker, Hyperliquid 4.5 bps, dYdX 5 bps tier-0, GMX 4 or 6 bps on the impact branch, gains.trade reads the openFeeP slot live from the on-chain Gains v8 fees contract. Spread plus impact at $1000 notional is added on top via an orderbook walk.
- slug: BTC
h2: "Cheapest perp DEX for BTC"
body: |
Expand All @@ -85,6 +92,10 @@ prometheus:
expected_freshness_seconds: 1800

faq:
- q: "Which perp DEX is cheapest for a $1M market order?"
a: "The venue with the deepest book, not the one with the lowest fee schedule. At $1,000,000 of notional the price impact from walking the book dwarfs the taker fee, so zero fee venues can lose to a 4 bps venue whose book absorbs the size better. The $1M panel on this page reruns the exact same measurement at that size, and the ranking regularly differs from the $1000 headline board. A venue that shows no data there means its visible book could not fill $1M at measurement time, which is a depth signal in itself."
- q: "Why does the ranking change with order size?"
a: "Two cost components move in opposite directions. The taker fee is a flat percentage, identical at any size. Price impact grows with size because a bigger order eats deeper into the book. Small orders are fee dominated, so zero fee venues win. Large orders are depth dominated, so the deepest book wins. The board at $1000 and the panels at $100k and $1M show all three regimes live."
- q: "What is the cheapest perpetual futures DEX right now?"
a: "{{best_name}} currently leads the leaderboard at {{best_p50}} all-in (24 h average) for opening a $1000 ETH long 10x position. The leaderboard re-sorts every five minutes against fresh Prometheus samples, so the answer reflects the last 24 hours of live data from each venue's public API, not a frozen rack-rate table."
- q: "Is Lighter really zero fees on perpetual futures?"
Expand Down Expand Up @@ -204,21 +215,50 @@ providers:
sample_size: count_over_time(perp_fees_all_in_bps{venue="gains"}[24h])
series: perp_fees_all_in_bps{venue="gains"}

# Notional-tier companion panels. Same pattern as perp-funding: the panel
# metric pins the asset label so the unfiltered "All" view reads one series
# per venue; on the BTC/SOL tabs the dimension injection skips chain=
# because it is already set, so the panels stay ETH-scoped by design.
# notional=1000 is not paneled: it duplicates the headline ledger.
- slug: paradex
name: Paradex
tag: Starknet appchain perps, 2 bps api taker tier
formula: "Average over 24h of (api-tier taker rate from the Paradex /markets fee config + half-spread plus impact from the /orderbook asks, depth 100, walked for $1000 of buy notional in the selected asset), in bps."
queries:
p50: avg_over_time(perp_fees_all_in_bps{venue="paradex"}[24h])
p90: quantile_over_time(0.90, perp_fees_all_in_bps{venue="paradex"}[24h])
p99: quantile_over_time(0.99, perp_fees_all_in_bps{venue="paradex"}[24h])
mean: avg_over_time(perp_fees_all_in_bps{venue="paradex"}[24h])
success: avg_over_time(perp_fees_health{venue="paradex"}[24h])
sample_size: count_over_time(perp_fees_all_in_bps{venue="paradex"}[24h])
series: perp_fees_all_in_bps{venue="paradex"}

- slug: extended
name: Extended
tag: Starknet perps, full public book, documented 2.5 bps taker
formula: "Average over 24h of (documented base taker rate of 2.5 bps, not exposed by Extended's public API, plus half-spread and impact from the full public orderbook walked for $1000 of buy notional in the selected asset), in bps."
queries:
p50: avg_over_time(perp_fees_all_in_bps{venue="extended"}[24h])
p90: quantile_over_time(0.90, perp_fees_all_in_bps{venue="extended"}[24h])
p99: quantile_over_time(0.99, perp_fees_all_in_bps{venue="extended"}[24h])
mean: avg_over_time(perp_fees_all_in_bps{venue="extended"}[24h])
success: avg_over_time(perp_fees_health{venue="extended"}[24h])
sample_size: count_over_time(perp_fees_all_in_bps{venue="extended"}[24h])
series: perp_fees_all_in_bps{venue="extended"}

# Notional-tier companion panels. The panel metric leaves the chain label
# unpinned and wraps the selector in avg(), so the ETH / BTC / SOL tab
# filter is injected exactly like on the headline queries and the panels
# follow the active tab; the unfiltered "All" view reads the cross-asset
# average, same semantics as the headline ledger. notional=1000 is not
# paneled (it duplicates the headline) and $10k reads within noise of
# $1k on every venue, so the panels show the two sizes where the story
# changes: $100k and $1M.
metric_panels:
- id: all_in_10k
label: "All-in at $10k (ETH)"
metric: perp_fees_all_in_bps_tier{chain="ETH",notional="10000"}
- id: all_in_100k
label: All-in at $100k
metric: avg(perp_fees_all_in_bps_tier{notional="100000"})
label_key: venue
unit: bps
description: "All-in opening cost measured at $10,000 notional on the ETH pair. Same formula as the headline (taker fee plus spread plus impact); only the orderbook walk depth changes."
- id: all_in_100k
label: "All-in at $100k (ETH)"
metric: perp_fees_all_in_bps_tier{chain="ETH",notional="100000"}
description: "All-in opening cost at $100,000 notional. Same formula as the headline (taker fee plus spread plus impact); price impact starts separating the books here and the zero fee lead narrows."
- id: all_in_1m
label: All-in at $1M
metric: avg(perp_fees_all_in_bps_tier{notional="1000000"})
label_key: venue
unit: bps
description: "All-in opening cost at $100,000 notional on the ETH pair. Books that cannot fill the size are skipped by the harness rather than extrapolated, so a venue missing here is itself a depth signal."
description: "All-in opening cost at $1,000,000 notional. At this size book depth decides the ranking and fee schedules become secondary. A venue whose visible book cannot absorb $1M shows no data instead of an extrapolated figure, which is itself a depth signal."
14 changes: 1 addition & 13 deletions benchmarks/perp-funding-stability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ abstract: |
upstream funding scrape (60 second poll, 30 second Prom scrape).

methodology:
- "Young venues read artificially stable at first: a venue whose funding has not moved since listing (Polymarket perps launched 2026-07-08 with a constant default rate) shows a stddev near zero until real funding history accrues. Read the sample count next to the rank."
- "Source. Same upstream as perp-funding: each venue's public funding endpoint polled every 60 seconds, normalized to bps per hour and per 24h hold, sign preserved."
- "Metric. stddev_over_time(perp_funding_hold_24h_bps{asset=\"ETH\"}[24h]) per venue. The 7d window smooths intraday noise but reacts to regime shifts within a week."
- "Sign handling. The underlying series is signed (positive means longs pay). Stability measures the swing, so the ranking uses the unsigned stddev. Average direction and level are the perp-funding bench."
Expand Down Expand Up @@ -212,19 +213,6 @@ providers:
sample_size: count_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h])
series: stddev_over_time(perp_venue_funding_24h_bps{venue="coinbase",asset="ETH"}[24h])

- slug: deribit
name: Deribit
tag: Options-focused CEX, ETH/BTC perpetuals via Mobula aggregator
formula: "stddev_over_time of perp_venue_funding_24h_bps{venue=\"deribit\",asset=\"ETH\"} over a 24h window. Source: Mobula CEFI funding-rate aggregator."
queries:
p50: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h])
p90: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="BTC"}[24h])
p99: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="SOL"}[24h])
mean: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h])
success: (perp_venue_last_refresh_unix{venue="deribit"} > bool (time() - 300))
sample_size: count_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h])
series: stddev_over_time(perp_venue_funding_24h_bps{venue="deribit",asset="ETH"}[24h])

- slug: gate
name: Gate
tag: CEX, USDT perpetuals via Mobula aggregator, 8h funding
Expand Down
16 changes: 8 additions & 8 deletions benchmarks/perp-funding.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="hyperliquid",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="hyperliquid",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="hyperliquid"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="hyperliquid"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="hyperliquid",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="hyperliquid",asset="ETH"}[24h])

- slug: binance
Expand All @@ -124,7 +124,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="binance",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="binance",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="binance"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="binance"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="binance",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="binance",asset="ETH"}[24h])

- slug: bybit
Expand All @@ -137,7 +137,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="bybit",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="bybit",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="bybit"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="bybit"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="bybit",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="bybit",asset="ETH"}[24h])

- slug: okx
Expand All @@ -150,7 +150,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="okx",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="okx",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="okx"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="okx"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="okx",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="okx",asset="ETH"}[24h])

- slug: dydx
Expand All @@ -163,7 +163,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="dydx",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="dydx",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="dydx"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="dydx"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="dydx",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="dydx",asset="ETH"}[24h])

- slug: paradex
Expand All @@ -176,7 +176,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="paradex",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="paradex",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="paradex"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="paradex"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="paradex",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="paradex",asset="ETH"}[24h])

- slug: polymarket
Expand All @@ -189,7 +189,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="polymarket",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="polymarket",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="polymarket"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="polymarket"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="polymarket",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="polymarket",asset="ETH"}[24h])

- slug: aster
Expand All @@ -202,7 +202,7 @@ providers:
p99: avg_over_time(perp_funding_hold_24h_bps{venue="aster",asset="SOL"}[24h])
mean: avg_over_time(perp_funding_annualized_pct{venue="aster",asset="ETH"}[24h])
success: (perp_funding_venue_ok_unix{venue="aster"} > bool (time() - 300))
sample_size: count(perp_funding_rate_hourly_bps{venue="aster"})
sample_size: count_over_time(perp_funding_hold_24h_bps{venue="aster",asset="ETH"}[24h])
series: avg_over_time(perp_funding_hold_24h_bps{venue="aster",asset="ETH"}[24h])

metric_panels:
Expand Down
Loading
Loading