malkhut: three-layer identifier architecture for cross-system asset identification
Layer 1 (canonical identity): symbol, base_asset, name, unified_symbol (CCXT format), quote_currency Layer 2 (cross-system): coingecko_id, cmc_id, blockchain, contract_address Layer 3 (exchange mapping): exchanges tuple All 13 pre-defined assets migrated with accurate CoinGecko IDs, CMC IDs, blockchains, and contract addresses (ERC-20 tokens). 7 new query functions: get_asset_by_coingecko_id, get_asset_by_cmc_id, get_assets_by_base_asset, get_assets_by_blockchain, get_assets_by_unified_symbol. 33 new tests covering: Layer 1 identity, Layer 2 cross-system identifiers, identifier query functions, identifier consistency (uniqueness, derivation), exchange registry. Total: 1189 tests, 47 files, all green. Based on research: CCXT BASE/QUOTE is de facto standard, CoinGecko ID most widely used in crypto-native, ISO 24165 DTI emerging, FIGI for institutional.
This commit is contained in:
@@ -191,10 +191,30 @@ def list_exchanges() -> List[str]:
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AssetProfile:
|
||||
"""Immutable asset classification. Sector & TokenRole are frozensets (multi-label).
|
||||
First element = primary label for single-label backward compatibility."""
|
||||
# --- Identity ---
|
||||
symbol: str
|
||||
"""Immutable asset classification. Three-layer identifier architecture:
|
||||
|
||||
Layer 1 — Canonical identity (venue-independent):
|
||||
symbol, base_asset, name
|
||||
|
||||
Layer 2 — Cross-system identifiers:
|
||||
coingecko_id, cmc_id, blockchain, contract_address
|
||||
|
||||
Layer 3 — Exchange market mapping:
|
||||
unified_symbol, quote_currency, exchanges
|
||||
|
||||
Sector & TokenRole are tuples (multi-label). First element = primary label."""
|
||||
# --- Layer 1: Canonical identity ---
|
||||
symbol: str # Internal canonical ID: "BTCUSDT"
|
||||
base_asset: str # Asset without quote: "BTC"
|
||||
name: str # Human-readable: "Bitcoin"
|
||||
unified_symbol: str # CCXT format: "BTC/USDT"
|
||||
quote_currency: str # Settlement: "USDT"
|
||||
|
||||
# --- Layer 2: Cross-system identifiers ---
|
||||
coingecko_id: str # "bitcoin" — most widely used in crypto-native
|
||||
cmc_id: int # 1 — CoinMarketCap numeric ID
|
||||
blockchain: str # Native chain: "bitcoin", "ethereum", "solana"
|
||||
contract_address: str # "0x..." for ERC-20, "" for native chain coins
|
||||
|
||||
# --- Fundamental (intrinsic, never changes) ---
|
||||
sectors: tuple[Sector, ...] # multi-label: BNB = (EXCHANGE, LAYER1). First = primary.
|
||||
@@ -290,12 +310,43 @@ def _profile(
|
||||
typical_spread_bps: float,
|
||||
typical_depth_usd: float,
|
||||
typical_daily_volume_usd: float,
|
||||
base_asset: str = "",
|
||||
name: str = "",
|
||||
unified_symbol: str = "",
|
||||
quote_currency: str = "USDT",
|
||||
coingecko_id: str = "",
|
||||
cmc_id: int = 0,
|
||||
blockchain: str = "",
|
||||
contract_address: str = "",
|
||||
has_funding: bool = False,
|
||||
has_options: bool = False,
|
||||
exchanges: Sequence[str] = ("binance",),
|
||||
) -> AssetProfile:
|
||||
# Auto-derive from symbol if not provided
|
||||
if not base_asset:
|
||||
# Strip quote currency suffix to get base asset
|
||||
for q in ("USDT", "USDC", "BUSD", "BTC", "ETH", "USD"):
|
||||
if symbol.endswith(q):
|
||||
base_asset = symbol[:-len(q)]
|
||||
break
|
||||
if not base_asset:
|
||||
base_asset = symbol
|
||||
if not name:
|
||||
name = base_asset
|
||||
if not unified_symbol:
|
||||
unified_symbol = f"{base_asset}/{quote_currency}"
|
||||
if not blockchain:
|
||||
blockchain = base_asset.lower()
|
||||
return AssetProfile(
|
||||
symbol=symbol,
|
||||
base_asset=base_asset,
|
||||
name=name,
|
||||
unified_symbol=unified_symbol,
|
||||
quote_currency=quote_currency,
|
||||
coingecko_id=coingecko_id or name.lower(),
|
||||
cmc_id=cmc_id,
|
||||
blockchain=blockchain,
|
||||
contract_address=contract_address,
|
||||
sectors=tuple(sectors),
|
||||
token_roles=tuple(token_roles),
|
||||
supply_model=supply_model,
|
||||
@@ -333,6 +384,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=0.3, typical_depth_usd=5_000_000,
|
||||
typical_daily_volume_usd=30_000_000_000,
|
||||
coingecko_id="bitcoin", cmc_id=1, blockchain="bitcoin", contract_address="",
|
||||
has_funding=True, has_options=True,
|
||||
),
|
||||
"ETHUSDT": _profile(
|
||||
@@ -349,6 +401,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=0.5, typical_depth_usd=3_000_000,
|
||||
typical_daily_volume_usd=15_000_000_000,
|
||||
coingecko_id="ethereum", cmc_id=1027, blockchain="ethereum", contract_address="",
|
||||
has_funding=True, has_options=True,
|
||||
),
|
||||
"SOLUSDT": _profile(
|
||||
@@ -365,6 +418,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.0, typical_depth_usd=1_000_000,
|
||||
typical_daily_volume_usd=3_000_000_000,
|
||||
coingecko_id="solana", cmc_id=5426, blockchain="solana", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
"DOGEUSDT": _profile(
|
||||
@@ -381,6 +435,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=2.0, typical_depth_usd=500_000,
|
||||
typical_daily_volume_usd=1_000_000_000,
|
||||
coingecko_id="dogecoin", cmc_id=74, blockchain="dogecoin", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
"ADAUSDT": _profile(
|
||||
@@ -397,6 +452,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.0, typical_depth_usd=800_000,
|
||||
typical_daily_volume_usd=500_000_000,
|
||||
coingecko_id="cardano", cmc_id=2010, blockchain="cardano", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
"AVAXUSDT": _profile(
|
||||
@@ -413,6 +469,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.2, typical_depth_usd=600_000,
|
||||
typical_daily_volume_usd=400_000_000,
|
||||
coingecko_id="avalanche-2", cmc_id=5805, blockchain="avalanche", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
"UNIUSDT": _profile(
|
||||
@@ -429,6 +486,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=2.0, typical_depth_usd=300_000,
|
||||
typical_daily_volume_usd=200_000_000,
|
||||
coingecko_id="uniswap", cmc_id=7083, blockchain="ethereum",
|
||||
contract_address="0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
|
||||
has_funding=True,
|
||||
),
|
||||
"LINKUSDT": _profile(
|
||||
@@ -445,6 +504,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.0, typical_depth_usd=700_000,
|
||||
typical_daily_volume_usd=400_000_000,
|
||||
coingecko_id="chainlink", cmc_id=1975, blockchain="ethereum",
|
||||
contract_address="0x514910771af9ca656af840dff83e8264ecf986ca",
|
||||
has_funding=True,
|
||||
),
|
||||
"BNBUSDT": _profile(
|
||||
@@ -461,6 +522,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.1, taker_fee_bps=0.4,
|
||||
typical_spread_bps=0.5, typical_depth_usd=2_000_000,
|
||||
typical_daily_volume_usd=2_000_000_000,
|
||||
coingecko_id="binancecoin", cmc_id=1839, blockchain="binance",
|
||||
contract_address="0xfb6312216814d55f56bee942ae2c4d04f1241e6b",
|
||||
has_funding=True,
|
||||
),
|
||||
"MATICUSDT": _profile(
|
||||
@@ -477,6 +540,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.5, typical_depth_usd=500_000,
|
||||
typical_daily_volume_usd=300_000_000,
|
||||
coingecko_id="matic-network", cmc_id=3890, blockchain="ethereum",
|
||||
contract_address="0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
|
||||
has_funding=True,
|
||||
),
|
||||
"AAVEUSDT": _profile(
|
||||
@@ -493,6 +558,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=2.5, typical_depth_usd=200_000,
|
||||
typical_daily_volume_usd=150_000_000,
|
||||
coingecko_id="aave", cmc_id=7278, blockchain="ethereum",
|
||||
contract_address="0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9",
|
||||
has_funding=True,
|
||||
),
|
||||
"DOTUSDT": _profile(
|
||||
@@ -509,6 +576,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=1.0, typical_depth_usd=600_000,
|
||||
typical_daily_volume_usd=300_000_000,
|
||||
coingecko_id="polkadot", cmc_id=6636, blockchain="polkadot", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
"ATOMUSDT": _profile(
|
||||
@@ -525,6 +593,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=2.0, typical_depth_usd=250_000,
|
||||
typical_daily_volume_usd=100_000_000,
|
||||
coingecko_id="cosmos", cmc_id=3794, blockchain="cosmos", contract_address="",
|
||||
has_funding=True,
|
||||
),
|
||||
}
|
||||
@@ -614,3 +683,37 @@ 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 ()
|
||||
|
||||
|
||||
def get_asset_by_coingecko_id(coingecko_id: str) -> Optional[AssetProfile]:
|
||||
"""Look up asset by CoinGecko ID (e.g., 'bitcoin', 'ethereum')."""
|
||||
for p in ASSET_PROFILES.values():
|
||||
if p.coingecko_id == coingecko_id:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def get_asset_by_cmc_id(cmc_id: int) -> Optional[AssetProfile]:
|
||||
"""Look up asset by CoinMarketCap numeric ID."""
|
||||
for p in ASSET_PROFILES.values():
|
||||
if p.cmc_id == cmc_id:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def get_assets_by_base_asset(base_asset: str) -> List[AssetProfile]:
|
||||
"""Find all profiles for a base asset (e.g., 'BTC' → BTCUSDT)."""
|
||||
return [p for p in ASSET_PROFILES.values() if p.base_asset == base_asset]
|
||||
|
||||
|
||||
def get_assets_by_blockchain(blockchain: str) -> List[AssetProfile]:
|
||||
"""Find all assets on a given blockchain."""
|
||||
return [p for p in ASSET_PROFILES.values() if p.blockchain == blockchain]
|
||||
|
||||
|
||||
def get_assets_by_unified_symbol(unified: str) -> Optional[AssetProfile]:
|
||||
"""Look up by CCXT unified symbol (e.g., 'BTC/USDT')."""
|
||||
for p in ASSET_PROFILES.values():
|
||||
if p.unified_symbol == unified:
|
||||
return p
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user