"""Symbol — canonical trading pair representation."""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, field_validator
__all__ = ["Symbol"]
_ALIASES: dict[str, str] = {"XBT": "BTC"}
_MARKETS: tuple[str, ...] = ("spot", "perp", "quarter", "next_quarter")
[docs]
class Symbol(BaseModel, frozen=True):
"""Canonical trading pair (base/quote, spot by default).
``market`` addresses non-spot derivative markets (``perp``, ``quarter``,
``next_quarter``); ``str()`` only appends the ``:<market>`` suffix when
it is not ``spot``, so existing spot-only code paths (storage layout,
job ids) are unaffected.
Examples
--------
>>> Symbol(base='BTC', quote='USDT')
Symbol(base='BTC', quote='USDT', market='spot')
>>> str(Symbol(base='BTC', quote='USDT'))
'BTC/USDT'
>>> str(Symbol(base='BTC', quote='USDT', market='perp'))
'BTC/USDT:perp'
>>> Symbol.parse('BTC/USDT')
Symbol(base='BTC', quote='USDT', market='spot')
>>> Symbol.parse('XBT/USD')
Symbol(base='BTC', quote='USD', market='spot')
>>> Symbol.parse('BTC/USDT:perp')
Symbol(base='BTC', quote='USDT', market='perp')
"""
base: str
quote: str
market: Literal["spot", "perp", "quarter", "next_quarter"] = "spot"
@field_validator("base", "quote", mode="before")
@classmethod
def _normalize(cls, v: str) -> str:
v = v.upper().strip()
return _ALIASES.get(v, v)
def __str__(self) -> str:
if self.market == "spot":
return f"{self.base}/{self.quote}"
return f"{self.base}/{self.quote}:{self.market}"
def __repr__(self) -> str:
return f"Symbol(base={self.base!r}, quote={self.quote!r}, market={self.market!r})"
[docs]
@classmethod
def parse(
cls,
raw: str,
market: Literal["spot", "perp", "quarter", "next_quarter"] = "spot",
) -> "Symbol":
"""Parse a pair string with an optional separator and market suffix.
Accepted formats: ``BTC/USDT``, ``BTC-USDT``, ``BTCUSDT`` (ambiguous,
use explicit separator when possible), each optionally followed by
``:<market>`` (e.g. ``BTC/USDT:perp``). An explicit suffix wins over
the ``market`` keyword argument.
Parameters
----------
raw : str
Pair string, optionally suffixed with ``:<market>``.
market : 'spot', 'perp', 'quarter', or 'next_quarter'
Market to use when ``raw`` carries no ``:<market>`` suffix.
Examples
--------
>>> Symbol.parse('ETH-USD')
Symbol(base='ETH', quote='USD', market='spot')
>>> Symbol.parse('ETH-USD:quarter')
Symbol(base='ETH', quote='USD', market='quarter')
"""
pair = raw
if ":" in raw:
pair, suffix = raw.split(":", 1)
if suffix not in _MARKETS:
raise ValueError(
f"Unknown market {suffix!r} in {raw!r}. "
f"Supported: {_MARKETS}"
)
market = suffix # type: ignore[assignment]
for sep in ("/", "-", "_"):
if sep in pair:
base, quote = pair.split(sep, 1)
return cls(base=base, quote=quote, market=market)
raise ValueError(
f"Cannot parse symbol {raw!r}: use a separator like '/' or '-'"
)