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:
Codex
2026-07-11 22:37:15 +02:00
parent 7bc13a6eb2
commit d2d0c5e292
3 changed files with 448 additions and 5 deletions

View File

@@ -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 ()