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
14 changes: 7 additions & 7 deletions benchmarks/perp-active-markets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ abstract: |
dYdX v4 indexer perpetualMarkets, Paradex markets summary, edgeX native markets,
Aster fapi exchangeInfo PERPETUAL instruments, Nado gateway, GRVT markets,
Extended public markets, Aevo markets, Pacifica Solana program state, Ostium
subgraph, Polymarket perps tickers). Gains uses the Mobula perp pairs
catalog (dex=gains, mainnet chains only, arbitrum-sepolia excluded), the same
source used by the perp-cohort-stats harness for all active_markets signals.
subgraph, Polymarket perps tickers). Gains reads its own Arbitrum trading
variables and leaves out the pairs it delisted, which stay in the array with
a 0.001x max leverage.
Higher is better.

methodology:
Expand All @@ -51,7 +51,7 @@ methodology:
- "Pacifica: onchain reads on the Solana program state, count of active perp markets."
- "Ostium: Arbitrum subgraph, count of active pairs. Covers FX and commodities alongside crypto."
- "Polymarket: public perps tickers endpoint on Polygon, count of listed instruments."
- "Gains: Mobula perp pairs catalog (GET /api/2/perp/pairs, dex=gains, mainnet only). Covers crypto, forex, stocks, indices and commodities across Arbitrum, Base, Polygon and ApeChain."
- "Gains: GET backend-arbitrum.gains.trade/trading-variables, every pair minus the ones whose max leverage override is 1 (0.001x), which is how Gains delists a pair without removing it. Covers crypto, forex, stocks, indices and commodities. Until 2026-10-12 this row read Mobula's perp pairs catalog, which listed 355 Gains pairs where 178 can be opened today, and which Mobula has since deprecated."
- "Headline: avg_over_time of the live market count gauge over the last 24 hours. Listings change infrequently so the 24h average is effectively the current count."
- "Failures: a venue that errors keeps its last gauge value and its perp_venue_health gauge drops toward 0; the leaderboard tags it as stale."

Expand Down Expand Up @@ -81,8 +81,8 @@ prometheus:
faq:
- q: "What does this benchmark measure?"
a: "The live count of active perpetual futures markets per venue. A market is a tradable instrument with a public price feed. Higher is better: more markets means more choice for traders."
- q: "Why does Gains use Mobula as a source instead of its native API?"
a: "The perp-cohort-stats harness uses the Mobula perp pairs catalog for gains because Mobula curates the full market taxonomy with asset-class tags. The count is mainnet-only (testnet chains excluded) and matches the inventory Gains exposes on its interface."
- q: "How is a delisted Gains pair told apart from a live one?"
a: "Gains never removes a pair from its trading variables; it delists one by setting the pair's max leverage override to 1, which is 0.001x at the contract's 1e3 precision (MATIC, LUNA, FTT, FB and a long tail of forex crosses and stocks carry it). The bench counts every other pair: an override of 0 means the group maximum applies, and a real override such as 2x or 5x on the stocks is still a market a trader can open."
- q: "Is market count a good proxy for venue breadth?"
a: "It is the simplest proxy. A venue with {{p50:gains}} markets covering crypto, forex, stocks and commodities (Gains) gives traders far more choice than one with {{p50:hyperliquid}} crypto-only markets (Hyperliquid). Depth per market is a separate signal; pair this bench with perp-volume-share and perp-fees for the full picture."
- q: "Can a venue with more asset classes rank below one with more instruments?"
Expand Down Expand Up @@ -117,7 +117,7 @@ providers:
- slug: gains
name: Gains
tag: Synthetic perps on Arbitrum, Base, Polygon and ApeChain, multi-asset
formula: "Count of active mainnet perp pairs from Mobula's catalog (dex=gains, testnet excluded). Covers crypto, forex, stocks, indices and commodities."
formula: "Pairs in the Arbitrum trading variables whose max leverage override is not 1 (0.001x, the delisting marker). Covers crypto, forex, stocks, indices and commodities."
queries:
p50: avg_over_time(perp_venue_active_markets{venue="gains"}[24h])
p90: quantile_over_time(0.90, perp_venue_active_markets{venue="gains"}[24h])
Expand Down
27 changes: 14 additions & 13 deletions benchmarks/perp-asset-breadth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ abstract: |
on that total. The class comes from the venue's own metadata when it
exists: Extended's category and subCategory, Paradex's RWA tag, Aster's
underlyingSubType (STOCK, ETF, Commodities, USD1-RWA), Ostium's pair
group, Ondo's tags, and the Mobula perp catalog assetClass for Gains and
Lighter. Hyperliquid's HIP-3 dexes (xyz, para, mkts, io and the others
returned by the perpDexs call) and dYdX publish no class, so their
symbols go through a fixed table of forex codes, commodity references
and index names; a symbol not in the table counts as a stock on a
HIP-3 dex and as crypto on dYdX, which carries an explicit list of its
group, Ondo's tags, and Gains' pair groups. Lighter, Hyperliquid's
HIP-3 dexes (xyz, para, mkts, io and the others returned by the
perpDexs call) and dYdX publish no class, so their symbols go through
a fixed table of forex codes, commodity references and index names; a
symbol not in the table counts as a stock on a HIP-3 dex and as crypto
on Lighter and on dYdX, which carries an explicit list of its
non-crypto tickers (EUR, TRY, WTI, XAG, TSLAX). GMX v2 is read from its
synthetic token list, where gold, silver, WTI, Brent, natural gas, SPY,
QQQ and a SpaceX pre-IPO synthetic sit next to the crypto markets.
Expand All @@ -67,14 +67,15 @@ methodology:
- "Ostium: pair group from the Ormi subgraph (crypto, forex, stocks, etf, indices, commodities); etf is split per symbol (URA, XLE, TLT count as stocks). Pairs with a last trade price only."
- "Ondo Perps: GET /v1/perps/contracts tags (Crypto, Stock, ETF, Commodity, Index, FX), enabled perpetual contracts only; ETF is split per symbol (SPY and QQQ count as indices, EWY, SOXL, URNM as stocks)."
- "dYdX v4: GET /v4/perpetualMarkets, ACTIVE markets. The indexer publishes no asset class and its tickers collide with the table (SPX-USD is the SPX6900 token, GME-USD a token), so a reviewed list marks EUR-USD and TRY-USD as forex, WTI-USD and XAG-USD as commodities, TSLAX-USD (xStocks TSLA) as a stock; everything else is crypto."
- "Gains and Lighter: Mobula GET /api/2/perp/pairs assetClass (forex, stocks, indices, commodities count; crypto, degen and new are crypto). Mainnet chains only, arbitrum-sepolia excluded."
- "Gains: the pair groups of its Arbitrum trading variables (forex, forex-minor and forex-exotic are forex; stocks-1 to 3 are stocks; indices; commodities-1 and 2; the three crypto groups are crypto), delisted pairs (max leverage 0.001x) left out. Read from Mobula's perp catalog until Mobula deprecated it on 2026-10-12."
- "Lighter publishes no class, so its symbols take the class another venue tags them with, then the fixed tables; a symbol no venue tags counts as crypto, so a pre-IPO synthetic only Lighter lists is undercounted, never invented. Read from Mobula's perp catalog until 2026-10-12."
- "GMX v2: GET arbitrum-api.gmxinfra.io/tokens, synthetic tokens matched against the table (GOLD, SILVER, WTIOIL, BRENTOIL, NATGAS are commodities, SPY and QQQ indices) plus a reviewed list for the equity synthetics the table cannot recognise (SPCX, SpaceX pre-IPO). MEGA (MegaETH) and SPX6900 stay crypto."
- "A venue whose catalog call fails on a tick keeps its previous count; the per-source error counter records the failure. Counts are not sampled, so p50, p90 and p99 read the same live value."
findings:
- "{{best_name}} leads non-crypto perp breadth with {{best_p50}} live markets across stocks, indices, forex and commodities, {{worst_name}} lists {{worst_p50}}."
- "Hyperliquid reaches {{p50:hyperliquid}} non-crypto markets through HIP-3 deployer dexes (xyz, para, mkts, io): US and Asian equities, ETFs, gold, silver, oil, copper, JPY and EUR, S&P 500 and Nasdaq 100 style indices. The core Hyperliquid universe is still crypto-only."
- "Extended lists {{p50:extended}} and Aster {{p50:aster}} non-crypto markets, both dominated by US stock perps, with a handful of commodities, indices and forex pairs on the side."
- "Gains lists {{p50:gains}} non-crypto markets, the broadest forex catalog of the cohort (18 pairs) next to 68 stocks, 14 indices and 10 commodities. Ostium lists {{p50:ostium}} with forex, indices and commodities as its core catalog rather than an add-on."
- "Gains lists {{p50:gains}} non-crypto markets, stocks first, then forex, indices and metals. Ostium lists {{p50:ostium}} with forex, indices and commodities as its core catalog rather than an add-on."
- "Ondo Perps lists {{p50:ondo}}, Lighter {{p50:lighter}}, Paradex {{p50:paradex}}. GMX v2 lists {{p50:gmx-v2}} (gold, silver, WTI, Brent, natural gas, SPY, QQQ, a SpaceX synthetic) and dYdX v4 {{p50:dydx}} live markets from its reviewed list (EUR, TRY, WTI, XAG, TSLAX, whichever are ACTIVE): the two older venues have added a handful of non-crypto markets, not a catalog."
source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-cohort-stats
prometheus:
Expand All @@ -85,7 +86,7 @@ faq:
- q: "What counts as a non-crypto market?"
a: "A perpetual whose underlying is not a crypto token: a stock or ETF (AAPL, TSLA, NVDA, EWY), an equity or rates index or the ETF that tracks one (S&P 500, SPY, Nasdaq 100, QQQ, KOSPI 200, US Treasuries), a forex pair (EUR/USD, USD/JPY) or a commodity (gold, silver, platinum, copper, WTI, Brent, natural gas). Tokenized gold such as PAXG or XAUT is an onchain token and stays in the crypto class."
- q: "Where does the asset class come from?"
a: "From the venue itself whenever it publishes one: Extended's category and subCategory, Paradex's RWA tag, Aster's underlyingSubType, Ostium's pair group, Ondo's tags, Mobula's assetClass for Gains and Lighter. Hyperliquid HIP-3 dexes and dYdX publish none, so their symbols are matched against a fixed table of forex codes, commodity references and index names, and dYdX carries a reviewed list of its five non-crypto tickers."
a: "From the venue itself whenever it publishes one: Extended's category and subCategory, Paradex's RWA tag, Aster's underlyingSubType, Ostium's pair group, Ondo's tags, Gains' pair groups. Lighter, Hyperliquid HIP-3 dexes and dYdX publish none, so their symbols are matched against a fixed table of forex codes, commodity references and index names, and dYdX carries a reviewed list of its five non-crypto tickers."
- q: "Why does Hyperliquid rank high when its own perps are crypto-only?"
a: "Because HIP-3 lets third-party deployers list perps on Hyperliquid's orderbook under a namespaced symbol (xyz:TSLA, mkts:US500, para:AVGO). Those markets settle on the same HyperBFT engine and are reachable from the same account, so the bench counts them as Hyperliquid markets. The core universe (BTC, ETH, SOL and the other core coins) contributes zero to the non-crypto count."
- q: "Does a market count if it only trades during exchange hours?"
Expand Down Expand Up @@ -134,8 +135,8 @@ providers:

- slug: gains
name: Gains
tag: Synthetic perps on Arbitrum, Base, Polygon and ApeChain, Mobula catalog
formula: "Count of mainnet Mobula catalog pairs for dex=gains where assetClass in {forex, stocks, indices, commodities}. Live."
tag: Synthetic perps on Arbitrum, Base, Polygon and ApeChain, native pair groups
formula: "Count of live Arbitrum pairs whose Gains group is forex, stocks, indices or commodities; delisted pairs (max leverage 0.001x) excluded. Live."
queries:
p50: perp_venue_noncore_markets_total{venue="gains"}
p90: perp_venue_noncore_markets_total{venue="gains"}
Expand Down Expand Up @@ -170,8 +171,8 @@ providers:

- slug: lighter
name: Lighter
tag: zk-rollup orderbook perps, Mobula catalog
formula: "Count of mainnet Mobula catalog pairs for dex=lighter where assetClass in {forex, stocks, indices, commodities}. Live."
tag: zk-rollup orderbook perps, cross-venue symbol classification
formula: "Count of active perp markets in orderBookDetails classified forex, stocks, indices or commodities: a symbol another venue tags as non-crypto takes that class, then the forex, commodity and index tables; any other symbol is crypto. Live."
queries:
p50: perp_venue_noncore_markets_total{venue="lighter"}
p90: perp_venue_noncore_markets_total{venue="lighter"}
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/perp-capital-efficiency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ abstract: |

methodology:
- "Cadence: every 5 minutes. Both active_markets and TVL are re-read on the same sweep cycle."
- "Active markets: native venue API per the perp-active-markets bench methodology. Gains: Mobula perp pairs catalog (mainnet only). Hyperliquid: info metaAndAssetCtxs. dYdX: indexer perpetualMarkets. Others: native endpoints."
- "Active markets: native venue API per the perp-active-markets bench methodology. Gains: its Arbitrum trading variables minus delisted pairs. Hyperliquid: info metaAndAssetCtxs. dYdX: indexer perpetualMarkets. Others: native endpoints."
- "TVL: DefiLlama /protocol/{slug} currentChainTvls. Staking and pool2 keys excluded. Multi-chain TVL summed across all deployment chains."
- "Gains TVL: GToken vault across Arbitrum, Base, Polygon and ApeChain. This is the shared liquidity pool that backs all synthetic pairs simultaneously."
- "Hyperliquid TVL: total user deposits (USDC). Funds serve as both margin and implicit market-maker collateral; every listed pair draws from this pool for maker fills."
Expand Down
2 changes: 1 addition & 1 deletion harnesses/perp-cohort-stats/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ reads on every SSR render of the perp pages.
| `perp_venue_volume_30d_usd{venue}` | DefiLlama HTML | (none) |
| `perp_venue_oi_usd{venue}` | HL native / Lighter native | DefiLlama HTML |
| `perp_venue_fees_30d_usd{venue}` | DefiLlama HTML | (none) |
| `perp_venue_active_markets{venue}` | HL native / Lighter native | DefiLlama HTML |
| `perp_venue_active_markets{venue}` | HL native / Lighter native / Gains trading variables (delisted pairs excluded) | DefiLlama HTML |
| `perp_venue_top_market_volume_24h_usd{venue}` | HL native / Lighter native | (none) |
| `perp_venue_health{venue}` | derived per tick | n/a |
| `perp_venue_funding_24h_bps{venue, asset}` | Mobula funding-rate | (none) |
Expand Down
26 changes: 16 additions & 10 deletions harnesses/perp-cohort-stats/cmd/script/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ const (
srcHLXyzNative = "hl_xyz_native"
srcCexCoinGecko = "cex_coingecko"
srcDefillama = "defillama"
srcMobulaPairs = "mobula_pairs"
srcMobulaFund = "mobula_funding"
)

Expand Down Expand Up @@ -317,13 +316,13 @@ func priorityMap(venue, metric string) []string {
return []string{srcHLNative}
case "lighter":
// Lighter native is the authoritative count: it filters
// market_type=="perp" + status=="active". Mobula pairs
// counts the whole catalog and diverges constantly here,
// so we keep it for `gains` (where Lighter does not
// publish) but drop it from the lighter priority list.
// market_type=="perp" + status=="active".
return []string{srcLighterNative}
case "gains":
return []string{srcMobulaPairs}
// The Arbitrum trading variables minus delisted pairs. Mobula's
// perp pairs catalog served this until Mobula deprecated it
// (404 since 2026-10-12).
return []string{srcGainsNative}
case "gmx-v2":
return []string{srcGMXNative, srcDefillama}
case "dydx":
Expand Down Expand Up @@ -438,15 +437,23 @@ func priorityMap(venue, metric string) []string {
return nil
}

// cryptoDefaultVenues are the venues whose unclassified symbols default
// to crypto: mainnet Lighter lists a few hundred tokens and a minority of
// stocks, forex and metals, the opposite of a HIP-3 RWA dex. A pre-IPO
// synthetic only Lighter lists therefore counts as crypto there, an
// undercount of its non-crypto catalog, never an overcount.
var cryptoDefaultVenues = map[string]bool{"lighter": true}

// Router orchestrates one sweep across all registered sources.
// classifyUnclassified turns every SourceResult.Unclassified list into a
// Breadth entry. Two sets accumulate across ticks (a source that failed
// this tick still contributes its last list): the symbols some venue
// lists as a non-crypto market and the symbols some venue lists as a
// crypto market. On a HIP-3 dex the RWA reading wins, then the crypto
// one (xyz:BOT is a token also listed on Lighter), then the fixed
// tables, and a symbol nobody knows counts as a stock. The Hyperliquid
// core universe arrives through the crypto map.
// tables, and a symbol nobody knows counts as a stock, except on a
// crypto-first venue (cryptoDefaultVenues) where it counts as crypto.
// The Hyperliquid core universe arrives through the crypto map.
func (r *Router) classifyUnclassified(byName map[string]*SourceResult) {
r.cryptoMu.Lock()
defer r.cryptoMu.Unlock()
Expand Down Expand Up @@ -479,7 +486,7 @@ func (r *Router) classifyUnclassified(byName map[string]*SourceResult) {
// that reading wins over a same-named token elsewhere.
class = rwaClass(sym)
} else {
class = symbolClass(sym, true, r.knownCrypto)
class = symbolClass(sym, !cryptoDefaultVenues[venue], r.knownCrypto)
}
b.add(class)
if class == classCrypto {
Expand Down Expand Up @@ -562,7 +569,6 @@ func NewRouter(cfg *Config) *Router {
if cfg.MobulaAPIKey != "" {
sources = append(sources,
NewMobulaFundingSource(cfg.MobulaAPIKey, cfg.MobulaFundingVenues),
NewMobulaPairsSource(cfg.MobulaAPIKey),
)
} else {
fmt.Println("[router] Mobula sources disabled (MOBULA_API_KEY unset)")
Expand Down
8 changes: 4 additions & 4 deletions harnesses/perp-cohort-stats/cmd/script/breadth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
// perp_venue_noncore_markets_total{venue} (bench perp-asset-breadth).
//
// Class names match the Mobula perp catalog taxonomy the bench started
// with (Gains and Lighter still come from Mobula), so a Mobula row and a
// native row are comparable:
// with (Mobula deprecated that catalog in 2026-10; Gains now reads its
// own pair groups and Lighter goes through the symbol path below):
//
// crypto tokens and crypto indices (TOTAL2, BTC dominance)
// forex fiat pairs (EUR/USD, USD/JPY)
Expand All @@ -28,8 +28,8 @@ import (
// own metadata decides when it exists (Extended category, Paradex tags,
// Aster underlyingSubType, Ostium group, Ondo tags); the symbol tables
// below only split a venue-tagged RWA bucket into forex, indices and
// commodities, and cover the two venues with no metadata at all
// (Hyperliquid HIP-3 dexes, dYdX).
// commodities, and cover the venues with no metadata at all
// (Hyperliquid HIP-3 dexes, dYdX, Lighter).
const (
classCrypto = "crypto"
classForex = "forex"
Expand Down
2 changes: 1 addition & 1 deletion harnesses/perp-cohort-stats/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func loadConfig() *Config {
}

if c.MobulaAPIKey == "" {
fmt.Println("[config] WARN: MOBULA_API_KEY is unset; Mobula funding + Mobula pairs sources will be skipped this run.")
fmt.Println("[config] WARN: MOBULA_API_KEY is unset; the Mobula funding source will be skipped this run.")
}

fmt.Printf("Config: venues=%d, tick=%v, mobula_venues=%q, mobula_key=%s\n",
Expand Down
2 changes: 1 addition & 1 deletion harnesses/perp-cohort-stats/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import (
func main() {
installLogCapture()
fmt.Println("=== perp-cohort-stats harness ===")
fmt.Println("Multi-source per-venue perp cohort stats (HL native, Lighter native, DefiLlama, Mobula funding + pairs).")
fmt.Println("Multi-source per-venue perp cohort stats (HL native, Lighter native, DefiLlama, Mobula funding).")
fmt.Println("Exposes /metrics, /health, /logs on :2112.")

cfg := loadConfig()
Expand Down
Loading
Loading