malkhut: multi-exchange asset universe + schema docs
ExchangeProfile: standardized exchange metadata (fees, latency, capabilities). 3 pre-defined exchanges: Binance, BingX, Bybit. AssetProfile.exchanges: tuple[str] — which venues trade each asset. New query functions: get_assets_on_exchange, get_common_assets, get_exchange_for_asset, get_exchange, list_exchanges. _DATA_STORAGE_SCHEMA_FORMATS.md: comprehensive reference for agent consumption — data model, storage format, query interfaces, data flow diagram, enum reference, import patterns for BLUE/VIOLET/UV integration. README updated: exchange registry section, package structure, subsystems table.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Asset Classification — INVARIANT characteristics only.
|
||||
Asset Classification — INVARIANT characteristics only. System-wide asset universe.
|
||||
|
||||
Split into two axes:
|
||||
FUNDAMENTAL — intrinsic to the token's design (never changes):
|
||||
@@ -8,6 +8,11 @@ Split into two axes:
|
||||
|
||||
TECHNICAL — invariant market-structure properties (set at listing, rarely change):
|
||||
MarketCapTier, TypicalSpread/Depth, TickSize/LotSize, FeeStructure,
|
||||
|
||||
Plus:
|
||||
EXCHANGE — multi-exchange support:
|
||||
ExchangeProfile (per-exchange metadata), exchange-asset mapping
|
||||
(which exchanges trade which assets, with exchange-specific parameters).
|
||||
DerivativeAccess, PriceDecimals, TypicalVolume
|
||||
|
||||
Overlap handling (industry standard, per CoinGecko/CMC/Messari):
|
||||
@@ -120,6 +125,66 @@ class LiquidityProfile(str, Enum):
|
||||
ILLIQUID = "illiquid" # <$1M
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ExchangeProfile — multi-exchange support
|
||||
# ==============================================================================
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExchangeProfile:
|
||||
"""Metadata for a trading venue. System-wide — used by BLUE/VIOLET/UV/MALKHUT."""
|
||||
exchange_id: str # canonical key: "binance", "bingx", "bybit", etc.
|
||||
display_name: str # human-readable: "Binance", "BingX"
|
||||
has_spot: bool
|
||||
has_perps: bool
|
||||
has_options: bool
|
||||
api_base_url: str # REST API root
|
||||
ws_base_url: str # WebSocket root ("" if not applicable)
|
||||
default_taker_fee_bps: float
|
||||
default_maker_fee_bps: float
|
||||
typical_latency_ms: float
|
||||
|
||||
|
||||
EXCHANGE_PROFILES: Dict[str, ExchangeProfile] = {}
|
||||
|
||||
BINANCE = ExchangeProfile(
|
||||
exchange_id="binance", display_name="Binance",
|
||||
has_spot=True, has_perps=True, has_options=True,
|
||||
api_base_url="https://api.binance.com",
|
||||
ws_base_url="wss://stream.binance.com:9443",
|
||||
default_taker_fee_bps=0.4, default_maker_fee_bps=0.2,
|
||||
typical_latency_ms=40,
|
||||
)
|
||||
EXCHANGE_PROFILES["binance"] = BINANCE
|
||||
|
||||
BINGX = ExchangeProfile(
|
||||
exchange_id="bingx", display_name="BingX",
|
||||
has_spot=True, has_perps=True, has_options=False,
|
||||
api_base_url="https://open-api.bingx.com",
|
||||
ws_base_url="wss://open-api.bingx.com/swapMarket",
|
||||
default_taker_fee_bps=0.5, default_maker_fee_bps=0.2,
|
||||
typical_latency_ms=100,
|
||||
)
|
||||
EXCHANGE_PROFILES["bingx"] = BINGX
|
||||
|
||||
BYBIT = ExchangeProfile(
|
||||
exchange_id="bybit", display_name="Bybit",
|
||||
has_spot=True, has_perps=True, has_options=True,
|
||||
api_base_url="https://api.bybit.com",
|
||||
ws_base_url="wss://stream.bybit.com/v5/public/linear",
|
||||
default_taker_fee_bps=0.06, default_maker_fee_bps=-0.01,
|
||||
typical_latency_ms=50,
|
||||
)
|
||||
EXCHANGE_PROFILES["bybit"] = BYBIT
|
||||
|
||||
|
||||
def get_exchange(exchange_id: str) -> Optional[ExchangeProfile]:
|
||||
return EXCHANGE_PROFILES.get(exchange_id)
|
||||
|
||||
|
||||
def list_exchanges() -> List[str]:
|
||||
return list(EXCHANGE_PROFILES.keys())
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# AssetProfile — frozen, all-invariant, multi-label where appropriate
|
||||
# ==============================================================================
|
||||
@@ -160,6 +225,9 @@ class AssetProfile:
|
||||
has_funding: bool = False
|
||||
has_options: bool = False
|
||||
|
||||
# --- Exchange membership (which venues trade this asset) ---
|
||||
exchanges: tuple[str, ...] = ("binance",) # tuple of exchange_ids
|
||||
|
||||
@property
|
||||
def sector(self) -> Sector:
|
||||
"""Primary sector (first in tuple). For single-label consumers."""
|
||||
@@ -224,6 +292,7 @@ def _profile(
|
||||
typical_daily_volume_usd: float,
|
||||
has_funding: bool = False,
|
||||
has_options: bool = False,
|
||||
exchanges: Sequence[str] = ("binance",),
|
||||
) -> AssetProfile:
|
||||
return AssetProfile(
|
||||
symbol=symbol,
|
||||
@@ -238,10 +307,10 @@ def _profile(
|
||||
derivative_access=derivative_access,
|
||||
tick_size=tick_size, lot_size=lot_size, price_decimals=price_decimals,
|
||||
maker_fee_bps=maker_fee_bps, taker_fee_bps=taker_fee_bps,
|
||||
typical_spread_bps=typical_spread_bps,
|
||||
typical_depth_usd=typical_depth_usd,
|
||||
typical_spread_bps=typical_spread_bps, typical_depth_usd=typical_depth_usd,
|
||||
typical_daily_volume_usd=typical_daily_volume_usd,
|
||||
has_funding=has_funding, has_options=has_options,
|
||||
exchanges=tuple(exchanges),
|
||||
)
|
||||
|
||||
|
||||
@@ -528,3 +597,20 @@ def get_multi_sector_assets() -> List[AssetProfile]:
|
||||
def get_multi_role_assets() -> List[AssetProfile]:
|
||||
"""Assets serving more than one token role."""
|
||||
return [p for p in ASSET_PROFILES.values() if len(p.token_roles) > 1]
|
||||
|
||||
|
||||
def get_assets_on_exchange(exchange_id: str) -> List[AssetProfile]:
|
||||
"""Assets traded on a given exchange."""
|
||||
return [p for p in ASSET_PROFILES.values() if exchange_id in p.exchanges]
|
||||
|
||||
|
||||
def get_common_assets(exchange_a: str, exchange_b: str) -> List[AssetProfile]:
|
||||
"""Assets traded on BOTH exchanges (intersection)."""
|
||||
return [p for p in ASSET_PROFILES.values()
|
||||
if exchange_a in p.exchanges and exchange_b in p.exchanges]
|
||||
|
||||
|
||||
def get_exchange_for_asset(symbol: str) -> tuple[str, ...]:
|
||||
"""Return which exchanges trade a given asset."""
|
||||
p = ASSET_PROFILES.get(symbol)
|
||||
return p.exchanges if p else ()
|
||||
|
||||
Reference in New Issue
Block a user