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-USDTfor spot againstBTC-USDT-SWAPfor 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 isBASE/QUOTE(HYPE/USDC), soBTC.HYPERLIQUIDandHYPE/USDC.HYPERLIQUIDcannot 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.
Sessions and market hours
The one market-hours law: week-minute session calendars keyed by venue and asset class, declared as DST unions that can only ever under-block.
Equity
The Equity asset class: what it means in Vike Trader's instrument taxonomy, which picker tab it appears under, and what it drives.