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

@@ -398,7 +398,7 @@ simple doctrinal tick-exits (C11) ship first via T19 step 3; MALKHUT supersedes
## DEVELOPMENT STATUS (2026-07-11) ## DEVELOPMENT STATUS (2026-07-11)
**1156 test functions. 47 test files. All green. 0 failures. 0 regressions.** **1189 test functions. 47 test files. All green. 0 failures. 0 regressions.**
### Completed subsystems ### Completed subsystems
@@ -620,15 +620,23 @@ Regimes: `trending_up`, `trending_down`, `high_volatility`, `low_volatility`,
Multi-label asset classification using **only invariant characteristics** — properties Multi-label asset classification using **only invariant characteristics** — properties
intrinsic to the token that predict price behaviour and order-book performance. intrinsic to the token that predict price behaviour and order-book performance.
**Three-layer identifier architecture** for cross-system interoperability.
**Design principle:** Classify by WHAT an asset IS (fundamental), not how it's trading **Design principle:** Classify by WHAT an asset IS (fundamental), not how it's trading
right now. Volatility/liquidity appear as long-run statistical profiles, not as right now. Volatility/liquidity appear as long-run statistical profiles, not as
classification axes that change minute-to-minute. classification axes that change minute-to-minute.
**Industry alignment:** CoinGecko/CMC use multi-tag taxonomy (ETH = "Smart Contracts" + #### Three-Layer Identifier Architecture
"SEC/CFTC Token Taxonomy"). BNB = "CEX Token" + "Smart Contracts" + "Layer 1". Our
system matches this: Sector and TokenRole are tuples (multi-label), first element = | Layer | Fields | Purpose | Example |
primary label for backward compatibility. |-------|--------|---------|---------|
| **1. Canonical** | `symbol`, `base_asset`, `name`, `unified_symbol`, `quote_currency` | Venue-independent identity | BTCUSDT / BTC / "Bitcoin" / BTC/USDT / USDT |
| **2. Cross-system** | `coingecko_id`, `cmc_id`, `blockchain`, `contract_address` | Data provider + on-chain mapping | "bitcoin" / 1 / "ethereum" / "" |
| **3. Exchange mapping** | `exchanges: tuple[str, ...]` | Which venues trade this asset | ("binance", "bingx", "bybit") |
**Industry standards:** CoinGecko `id` (most widely used in crypto-native),
CoinMarketCap numeric `id` (second most used), ISO 24165 DTI (emerging), FIGI
(institutional), CCXT `BASE/QUOTE` format (de facto trading standard).
#### Fundamental Dimensions (intrinsic, never change) #### Fundamental Dimensions (intrinsic, never change)

View File

@@ -2,19 +2,24 @@
Tests for asset classification — multi-label, invariant characteristics, exhaustive. Tests for asset classification — multi-label, invariant characteristics, exhaustive.
Covers: per-asset profiles (all 13), multi-label overlap, cross-dimensional Covers: per-asset profiles (all 13), multi-label overlap, cross-dimensional
consistency, enum coverage, query edge cases, orderbook fingerprint ordering, consistency, enum coverage, query edge cases, orderbook fingerprint ordering,
predictive properties, and ScenarioFactory integration. predictive properties, ScenarioFactory integration, and layered identifiers.
""" """
import pytest import pytest
from malkhut.training.asset_classification import ( from malkhut.training.asset_classification import (
Sector, TokenRole, SupplyModel, ConsensusFamily, SmartContractCapability, Sector, TokenRole, SupplyModel, ConsensusFamily, SmartContractCapability,
MarketCapTier, DerivativeAccess, VolatilityProfile, LiquidityProfile, MarketCapTier, DerivativeAccess, VolatilityProfile, LiquidityProfile,
AssetProfile, ASSET_PROFILES, AssetProfile, ASSET_PROFILES,
ExchangeProfile, EXCHANGE_PROFILES, get_exchange, list_exchanges,
get_asset_profile, list_assets, get_asset_profile, list_assets,
get_assets_by_sector, get_assets_by_token_role, get_assets_by_supply, get_assets_by_sector, get_assets_by_token_role, get_assets_by_supply,
get_assets_by_consensus, get_assets_by_market_cap, get_assets_by_volatility, get_assets_by_consensus, get_assets_by_market_cap, get_assets_by_volatility,
get_assets_by_liquidity, get_assets_by_derivatives, get_gas_tokens, get_assets_by_liquidity, get_assets_by_derivatives, get_gas_tokens,
get_pov_assets, get_shortable_assets, get_pov_assets, get_shortable_assets,
get_multi_sector_assets, get_multi_role_assets, get_multi_sector_assets, get_multi_role_assets,
get_assets_on_exchange, get_common_assets, get_exchange_for_asset,
get_asset_by_coingecko_id, get_asset_by_cmc_id,
get_assets_by_base_asset, get_assets_by_blockchain,
get_assets_by_unified_symbol,
) )
from malkhut.training.cma_trainer import ScenarioFactory from malkhut.training.cma_trainer import ScenarioFactory
@@ -996,3 +1001,200 @@ class TestMultiAssetScenarios:
suite = factory.build_suite(symbols=("ETHUSDT",), steps_per_scenario=10) suite = factory.build_suite(symbols=("ETHUSDT",), steps_per_scenario=10)
for s in suite: for s in suite:
assert s.max_steps >= 10 assert s.max_steps >= 10
# ==============================================================================
# Layered Identifier Architecture Tests
# ==============================================================================
class TestLayeredIdentifiers:
"""Layer 1: Canonical identity."""
def test_all_have_base_asset(self):
for p in ASSET_PROFILES.values():
assert p.base_asset, f'{p.symbol}: empty base_asset'
def test_all_have_name(self):
for p in ASSET_PROFILES.values():
assert p.name, f'{p.symbol}: empty name'
def test_all_have_unified_symbol(self):
for p in ASSET_PROFILES.values():
assert '/' in p.unified_symbol, f'{p.symbol}: unified missing /'
base, quote = p.unified_symbol.split('/')
assert base == p.base_asset
assert quote == p.quote_currency
def test_btc_identity(self):
p = get_asset_profile("BTCUSDT")
assert p.base_asset == "BTC"
assert p.name == "BTC"
assert p.unified_symbol == "BTC/USDT"
assert p.quote_currency == "USDT"
def test_eth_identity(self):
p = get_asset_profile("ETHUSDT")
assert p.base_asset == "ETH"
assert p.unified_symbol == "ETH/USDT"
def test_doge_identity(self):
p = get_asset_profile("DOGEUSDT")
assert p.base_asset == "DOGE"
assert p.unified_symbol == "DOGE/USDT"
class TestCrossSystemIdentifiers:
"""Layer 2: Cross-system identifiers."""
def test_all_have_coingecko_id(self):
for p in ASSET_PROFILES.values():
assert p.coingecko_id, f'{p.symbol}: empty coingecko_id'
def test_all_have_cmc_id(self):
for p in ASSET_PROFILES.values():
assert p.cmc_id > 0, f'{p.symbol}: invalid cmc_id'
def test_all_have_blockchain(self):
for p in ASSET_PROFILES.values():
assert p.blockchain, f'{p.symbol}: empty blockchain'
def test_btc_identifiers(self):
p = get_asset_profile("BTCUSDT")
assert p.coingecko_id == "bitcoin"
assert p.cmc_id == 1
assert p.blockchain == "bitcoin"
assert p.contract_address == ""
def test_eth_identifiers(self):
p = get_asset_profile("ETHUSDT")
assert p.coingecko_id == "ethereum"
assert p.cmc_id == 1027
assert p.blockchain == "ethereum"
def test_uni_has_contract_address(self):
p = get_asset_profile("UNIUSDT")
assert p.contract_address.startswith("0x")
assert p.blockchain == "ethereum"
def test_link_has_contract_address(self):
p = get_asset_profile("LINKUSDT")
assert p.contract_address.startswith("0x")
def test_native_coins_have_empty_address(self):
"""Native chain coins (BTC, ETH, SOL, etc.) have no contract address."""
for sym in ("BTCUSDT", "ETHUSDT", "SOLUSDT", "DOGEUSDT", "ADAUSDT",
"AVAXUSDT", "DOTUSDT", "ATOMUSDT"):
p = get_asset_profile(sym)
assert p.contract_address == "", f'{sym}: native coin should have empty address'
class TestIdentifierQueryFunctions:
"""Query by layered identifiers."""
def test_coingecko_query(self):
btc = get_asset_by_coingecko_id("bitcoin")
assert btc is not None
assert btc.symbol == "BTCUSDT"
def test_coingecko_miss(self):
assert get_asset_by_coingecko_id("nonexistent") is None
def test_cmc_query(self):
eth = get_asset_by_cmc_id(1027)
assert eth is not None
assert eth.symbol == "ETHUSDT"
def test_cmc_miss(self):
assert get_asset_by_cmc_id(99999) is None
def test_base_asset_query(self):
uni = get_assets_by_base_asset("UNI")
assert len(uni) == 1
assert uni[0].symbol == "UNIUSDT"
def test_blockchain_query(self):
eth_chain = get_assets_by_blockchain("ethereum")
symbols = {p.symbol for p in eth_chain}
assert "ETHUSDT" in symbols
assert "UNIUSDT" in symbols
assert "LINKUSDT" in symbols
assert "AAVEUSDT" in symbols
assert "MATICUSDT" in symbols
assert len(eth_chain) >= 5
def test_unified_symbol_query(self):
p = get_assets_by_unified_symbol("BTC/USDT")
assert p is not None
assert p.symbol == "BTCUSDT"
def test_unified_symbol_miss(self):
assert get_assets_by_unified_symbol("BTC/EUR") is None
class TestIdentifierConsistency:
"""Cross-checks: identifiers must be internally consistent."""
def test_symbol_matches_unified_base(self):
"""symbol[:-len(quote)] should equal unified base."""
for p in ASSET_PROFILES.values():
expected_base = p.symbol.replace(p.quote_currency, "")
assert p.base_asset == expected_base, \
f'{p.symbol}: base_asset={p.base_asset} != expected {expected_base}'
def test_coingecko_ids_unique(self):
ids = [p.coingecko_id for p in ASSET_PROFILES.values()]
assert len(ids) == len(set(ids)), "Duplicate coingecko_ids"
def test_cmc_ids_unique(self):
ids = [p.cmc_id for p in ASSET_PROFILES.values()]
assert len(ids) == len(set(ids)), "Duplicate cmc_ids"
def test_blockchain_grouping(self):
"""Same-chain assets should share blockchain field."""
btc_chain = get_assets_by_blockchain("bitcoin")
assert len(btc_chain) == 1 # only BTC
eth_chain = get_assets_by_blockchain("ethereum")
assert len(eth_chain) >= 4 # ETH, UNI, LINK, MATIC, AAVE
class TestExchangeRegistry:
"""ExchangeProfile tests."""
def test_exchanges_registered(self):
assert len(EXCHANGE_PROFILES) >= 3
def test_binance_profile(self):
ex = get_exchange("binance")
assert ex is not None
assert ex.display_name == "Binance"
assert ex.has_spot
assert ex.has_perps
assert ex.has_options
assert ex.default_taker_fee_bps > 0
assert ex.typical_latency_ms > 0
def test_bingx_profile(self):
ex = get_exchange("bingx")
assert ex is not None
assert ex.display_name == "BingX"
assert not ex.has_options
def test_bybit_profile(self):
ex = get_exchange("bybit")
assert ex is not None
assert ex.has_options
def test_list_exchanges(self):
exs = list_exchanges()
assert "binance" in exs
assert "bingx" in exs
assert "bybit" in exs
def test_get_exchange_miss(self):
assert get_exchange("nonexistent") is None
def test_exchange_fields_frozen(self):
for ex in EXCHANGE_PROFILES.values():
with pytest.raises(AttributeError):
ex.display_name = "X"

View File

@@ -69,8 +69,18 @@ EXCHANGE_PROFILES["okx"] = ExchangeProfile(
```python ```python
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class AssetProfile: class AssetProfile:
# Identity # --- Layer 1: Canonical identity (venue-independent) ---
symbol: str # "BTCUSDT" symbol: str # "BTCUSDT" — internal canonical ID
base_asset: str # "BTC" — asset without quote
name: str # "Bitcoin" — human-readable
unified_symbol: str # "BTC/USDT" — CCXT format
quote_currency: str # "USDT" — settlement currency
# --- Layer 2: Cross-system identifiers ---
coingecko_id: str # "bitcoin" — CoinGecko slug
cmc_id: int # 1 — CoinMarketCap numeric ID
blockchain: str # "bitcoin" — native chain
contract_address: str # "0x..." for ERC-20, "" for native coins
# Fundamental (intrinsic, never change) # Fundamental (intrinsic, never change)
sectors: tuple[Sector, ...] # ("CURRENCY",) sectors: tuple[Sector, ...] # ("CURRENCY",)
@@ -101,7 +111,7 @@ class AssetProfile:
has_funding: bool = False has_funding: bool = False
has_options: bool = False has_options: bool = False
# Exchange membership # --- Layer 3: Exchange membership ---
exchanges: tuple[str, ...] = ("binance",) # which venues trade this exchanges: tuple[str, ...] = ("binance",) # which venues trade this
``` ```
@@ -118,6 +128,11 @@ class AssetProfile:
|----------|---------| |----------|---------|
| `get_asset_profile(symbol)` | Single profile or None | | `get_asset_profile(symbol)` | Single profile or None |
| `list_assets()` | All symbols | | `list_assets()` | All symbols |
| `get_asset_by_coingecko_id(id)` | Profile by CoinGecko slug |
| `get_asset_by_cmc_id(id)` | Profile by CMC numeric ID |
| `get_assets_by_base_asset(base)` | All profiles for a base asset |
| `get_assets_by_blockchain(chain)` | Assets on a given chain |
| `get_assets_by_unified_symbol(sym)` | Profile by CCXT unified symbol |
| `get_assets_by_sector(sector)` | Assets in ANY of the queried sector | | `get_assets_by_sector(sector)` | Assets in ANY of the queried sector |
| `get_assets_by_token_role(role)` | Assets with ANY of the queried role | | `get_assets_by_token_role(role)` | Assets with ANY of the queried role |
| `get_assets_by_supply(model)` | Assets with given supply model | | `get_assets_by_supply(model)` | Assets with given supply model |

View File

@@ -191,10 +191,30 @@ def list_exchanges() -> List[str]:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class AssetProfile: class AssetProfile:
"""Immutable asset classification. Sector & TokenRole are frozensets (multi-label). """Immutable asset classification. Three-layer identifier architecture:
First element = primary label for single-label backward compatibility."""
# --- Identity --- Layer 1 — Canonical identity (venue-independent):
symbol: str 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) --- # --- Fundamental (intrinsic, never changes) ---
sectors: tuple[Sector, ...] # multi-label: BNB = (EXCHANGE, LAYER1). First = primary. sectors: tuple[Sector, ...] # multi-label: BNB = (EXCHANGE, LAYER1). First = primary.
@@ -290,12 +310,43 @@ def _profile(
typical_spread_bps: float, typical_spread_bps: float,
typical_depth_usd: float, typical_depth_usd: float,
typical_daily_volume_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_funding: bool = False,
has_options: bool = False, has_options: bool = False,
exchanges: Sequence[str] = ("binance",), exchanges: Sequence[str] = ("binance",),
) -> AssetProfile: ) -> 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( return AssetProfile(
symbol=symbol, 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), sectors=tuple(sectors),
token_roles=tuple(token_roles), token_roles=tuple(token_roles),
supply_model=supply_model, supply_model=supply_model,
@@ -333,6 +384,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=0.3, typical_depth_usd=5_000_000, typical_spread_bps=0.3, typical_depth_usd=5_000_000,
typical_daily_volume_usd=30_000_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, has_funding=True, has_options=True,
), ),
"ETHUSDT": _profile( "ETHUSDT": _profile(
@@ -349,6 +401,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=0.5, typical_depth_usd=3_000_000, typical_spread_bps=0.5, typical_depth_usd=3_000_000,
typical_daily_volume_usd=15_000_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, has_funding=True, has_options=True,
), ),
"SOLUSDT": _profile( "SOLUSDT": _profile(
@@ -365,6 +418,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.0, typical_depth_usd=1_000_000, typical_spread_bps=1.0, typical_depth_usd=1_000_000,
typical_daily_volume_usd=3_000_000_000, typical_daily_volume_usd=3_000_000_000,
coingecko_id="solana", cmc_id=5426, blockchain="solana", contract_address="",
has_funding=True, has_funding=True,
), ),
"DOGEUSDT": _profile( "DOGEUSDT": _profile(
@@ -381,6 +435,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=2.0, typical_depth_usd=500_000, typical_spread_bps=2.0, typical_depth_usd=500_000,
typical_daily_volume_usd=1_000_000_000, typical_daily_volume_usd=1_000_000_000,
coingecko_id="dogecoin", cmc_id=74, blockchain="dogecoin", contract_address="",
has_funding=True, has_funding=True,
), ),
"ADAUSDT": _profile( "ADAUSDT": _profile(
@@ -397,6 +452,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.0, typical_depth_usd=800_000, typical_spread_bps=1.0, typical_depth_usd=800_000,
typical_daily_volume_usd=500_000_000, typical_daily_volume_usd=500_000_000,
coingecko_id="cardano", cmc_id=2010, blockchain="cardano", contract_address="",
has_funding=True, has_funding=True,
), ),
"AVAXUSDT": _profile( "AVAXUSDT": _profile(
@@ -413,6 +469,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.2, typical_depth_usd=600_000, typical_spread_bps=1.2, typical_depth_usd=600_000,
typical_daily_volume_usd=400_000_000, typical_daily_volume_usd=400_000_000,
coingecko_id="avalanche-2", cmc_id=5805, blockchain="avalanche", contract_address="",
has_funding=True, has_funding=True,
), ),
"UNIUSDT": _profile( "UNIUSDT": _profile(
@@ -429,6 +486,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=2.0, typical_depth_usd=300_000, typical_spread_bps=2.0, typical_depth_usd=300_000,
typical_daily_volume_usd=200_000_000, typical_daily_volume_usd=200_000_000,
coingecko_id="uniswap", cmc_id=7083, blockchain="ethereum",
contract_address="0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
has_funding=True, has_funding=True,
), ),
"LINKUSDT": _profile( "LINKUSDT": _profile(
@@ -445,6 +504,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.0, typical_depth_usd=700_000, typical_spread_bps=1.0, typical_depth_usd=700_000,
typical_daily_volume_usd=400_000_000, typical_daily_volume_usd=400_000_000,
coingecko_id="chainlink", cmc_id=1975, blockchain="ethereum",
contract_address="0x514910771af9ca656af840dff83e8264ecf986ca",
has_funding=True, has_funding=True,
), ),
"BNBUSDT": _profile( "BNBUSDT": _profile(
@@ -461,6 +522,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.1, taker_fee_bps=0.4, maker_fee_bps=-0.1, taker_fee_bps=0.4,
typical_spread_bps=0.5, typical_depth_usd=2_000_000, typical_spread_bps=0.5, typical_depth_usd=2_000_000,
typical_daily_volume_usd=2_000_000_000, typical_daily_volume_usd=2_000_000_000,
coingecko_id="binancecoin", cmc_id=1839, blockchain="binance",
contract_address="0xfb6312216814d55f56bee942ae2c4d04f1241e6b",
has_funding=True, has_funding=True,
), ),
"MATICUSDT": _profile( "MATICUSDT": _profile(
@@ -477,6 +540,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.5, typical_depth_usd=500_000, typical_spread_bps=1.5, typical_depth_usd=500_000,
typical_daily_volume_usd=300_000_000, typical_daily_volume_usd=300_000_000,
coingecko_id="matic-network", cmc_id=3890, blockchain="ethereum",
contract_address="0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
has_funding=True, has_funding=True,
), ),
"AAVEUSDT": _profile( "AAVEUSDT": _profile(
@@ -493,6 +558,8 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=2.5, typical_depth_usd=200_000, typical_spread_bps=2.5, typical_depth_usd=200_000,
typical_daily_volume_usd=150_000_000, typical_daily_volume_usd=150_000_000,
coingecko_id="aave", cmc_id=7278, blockchain="ethereum",
contract_address="0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9",
has_funding=True, has_funding=True,
), ),
"DOTUSDT": _profile( "DOTUSDT": _profile(
@@ -509,6 +576,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=1.0, typical_depth_usd=600_000, typical_spread_bps=1.0, typical_depth_usd=600_000,
typical_daily_volume_usd=300_000_000, typical_daily_volume_usd=300_000_000,
coingecko_id="polkadot", cmc_id=6636, blockchain="polkadot", contract_address="",
has_funding=True, has_funding=True,
), ),
"ATOMUSDT": _profile( "ATOMUSDT": _profile(
@@ -525,6 +593,7 @@ ASSET_PROFILES: Dict[str, AssetProfile] = {
maker_fee_bps=-0.2, taker_fee_bps=0.5, maker_fee_bps=-0.2, taker_fee_bps=0.5,
typical_spread_bps=2.0, typical_depth_usd=250_000, typical_spread_bps=2.0, typical_depth_usd=250_000,
typical_daily_volume_usd=100_000_000, typical_daily_volume_usd=100_000_000,
coingecko_id="cosmos", cmc_id=3794, blockchain="cosmos", contract_address="",
has_funding=True, has_funding=True,
), ),
} }
@@ -614,3 +683,37 @@ def get_exchange_for_asset(symbol: str) -> tuple[str, ...]:
"""Return which exchanges trade a given asset.""" """Return which exchanges trade a given asset."""
p = ASSET_PROFILES.get(symbol) p = ASSET_PROFILES.get(symbol)
return p.exchanges if p else () 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