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:
Codex
2026-07-12 00:40:14 +02:00
parent da61c8f6c2
commit 1f709af6d1
4 changed files with 341 additions and 13 deletions

View File

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