vike
ConceptsInstruments

Symbology

Core symbols versus exchange symbols, the .P perpetual suffix and which venues use it, and the declarative field map that turns a venue payload into instruments.

A core symbol names an instrument unambiguously across the whole workspace; an exchange symbol is what goes on the wire. For most instruments they are identical, and crates/vike-catalog/src/symbol.rs owns the conversion in one place.

Where the two diverge

They diverge for perpetuals on venues whose raw symbol does not distinguish a perp from its spot twin. Binance lists BTCUSDT on spot and BTCUSDT as a USDⓈ-M perp, so the core vocabulary appends PERP_SUFFIX — .P — and calls the perp BTCUSDT.P. Siblings exist in the same convention (.F for futures, .O for options), but the constant's doc records that no venue adapter maps them today, so only .P is named.

split_perp is the conversion: "BTCUSDT.P" becomes ("BTCUSDT", true), "BTCUSDT" becomes ("BTCUSDT", false). The returned symbol borrows from the input and is the wire form — URLs, stream names, REST params — while the caller's original suffixed string stays the series and sink label, so a perp's series key never collides with its spot twin's. It is a trailing marker only: BTC.PERP and XPUSDT are not perps.

Why this is a module and not an idiom

It was an idiom, written out at eight call sites across three bridges, twice behind a local helper under a different name. Every one of them was correct. The bug that motivated the module is the site that did none of it: Binance's depth feed passed the raw core symbol straight into its stream-name builder, producing btcusdt.p@depth@100ms — a stream no venue resolves. It connected, streamed nothing, and the recorder wrote placeholder rows for hours without a single error. An idiom repeated eight times cannot be checked; a named function with one home can be grepped for, and its absence at a wire-facing call site is visible.

Which venues suffix

uses_perp_suffix answers per venue, and today the suffixing venues are binance, bybit and aster. It is declared rather than inferred, so a new venue trips the roster test until somebody decides which side it is on. false does not mean "no perps" — it means the venue's own symbols are already unambiguous, so a suffix would be noise the adapter would immediately strip again. Two worked cases, both read from their adapters:

  • OKX uses dashed instIds, BTC-USDT for spot against BTC-USDT-SWAP for the swap — a key namespace its catalog notes is distinct from Binance's concatenated one, so chart keys never collide (crates/bridges/okx/src/catalog.rs).
  • Hyperliquid puts perps and spot in disjoint namespaces outright: a perp is the bare coin (BTC), a spot pair is BASE/QUOTE (HYPE/USDC), so BTC.HYPERLIQUID and HYPE/USDC.HYPERLIQUID cannot collide and no suffix is applied (crates/bridges/hyperliquid/src/catalog.rs).

The fee lane

The same split keys the fee table. fee_lane(venue, symbol) returns the venue unchanged for a single-lane venue, and a lane sub-key — binance-perp, aster-perp, aster-perp-usd1 — for a .P symbol on a dual-lane venue. A venue is dual-lane exactly when its exec.rs branches on the suffix. Bybit uses the suffix but its exec is V5 linear-perp only, with no spot arm to route to, so its single fee row already is its perp row.

Aster has three lanes rather than two: a live sweep of the venue's own GET /fapi/v3/commissionRate across 17 symbols on 2026-08-05 found crypto at 4 bps, equity/ETF/ commodity at 0.9 bps and USD1-settled at 0.5 bps. Only the USD1 class is decidable from the symbol, because the settlement asset is part of the name (BTCUSD1 settles in USD1, BTCUSDT in USDT). The equity class is deliberately not resolved: AAPLUSDT and ASTERUSDT are the same string shape, and only the venue's exchangeInfo separates them, so a pure string function has no honest way to know.

USD1 is matched as a suffix, never a substring

The match runs against the exchange symbol after the .P strip. USD1USDT is USDT-settled with USD1 as the base asset and must keep the crypto lane.

The field map

Most Enumerable venues serve a flat JSON list, and crates/vike-catalog/src/field_map.rs turns that into a declaration rather than a parser. A FieldMap names the list_path to walk, the symbol, base and quote fields, and an optional active gate given as a (field, expected) pair — rows whose field does not equal the expectation are skipped. parse_with produces Instruments, upper-casing symbol, base and quote, and returns an empty vector for a missing or malformed payload rather than panicking. Venues whose payloads are nested, computed or protobuf skip the map and provide their own parse function.

Binance and Aster share the same exchangeInfo grammar, so the mapping lives once in crates/bridges/binance/src/family/catalog.rs, with the venue a caller-supplied parameter and the endpoint URLs deliberately outside the module. Its SPOT_MAP walks ["symbols"] and reads symbol, baseAsset, quoteAsset, gated on status == "TRADING". Bybit's SPOT_MAP walks ["result", "list"] and reads symbol, baseCoin, quoteCoin, gated on status == "Trading".

Perp parsing is where the map runs out. Both venues need a second condition, each for its own reason: Binance gates on contractType == "PERPETUAL" because delivery futures share its futures endpoint, Bybit on "LinearPerpetual" because its linear category also lists inverse and other contract types. So each is a small function instead, the documented escape hatch for a venue whose "active" test needs more than one field. Binance's perp parser is also where the .P is stamped on. Which venues carry which classes is on the venue pages.

On this page