malkhut: asset classification, behavior DSL, auto-compiler, behavior-driven scenarios
Cambrian Explosion Phase 0 complete: - Multi-label invariant asset taxonomy (13 assets, 10 sectors, 6 roles) - Asset Behavior DSL: 10 orthogonal dimensions per asset, 3 composable templates, research-validated from live Binance/BingX API data - Asset Compiler: auto-fetch from Binance public API, compute profiles, rate-limited (1 req/s), cached, 28 known classifications - ScenarioFactory: all 30 scenario types use behavior-driven prices (BTC=$64K, ETH=$1.8K, SOL=$80, DOGE=$0.07) instead of hardcoded BTC prices. Auto-compiles unknown assets on demand. - Label query interfaces: build_suite_for_sector/role/template/vol/labels - 190 exhaustive tests for asset classification (up from 66) - 1140 tests all green, CWM throughput 189K calls/s (121% of baseline) - Comprehensive README: 1043 lines with full documentation Research sources: Binance live REST API, BingX open API, CoinGlass, academic literature (Bouchaud, Cont/Stoikov, Cartea/Jaimungal)
This commit is contained in:
1043
MALKHUT/README.md
Normal file
1043
MALKHUT/README.md
Normal file
File diff suppressed because it is too large
Load Diff
998
MALKHUT/malkhut/tests/test_asset_classification.py
Normal file
998
MALKHUT/malkhut/tests/test_asset_classification.py
Normal file
@@ -0,0 +1,998 @@
|
||||
"""
|
||||
Tests for asset classification — multi-label, invariant characteristics, exhaustive.
|
||||
Covers: per-asset profiles (all 13), multi-label overlap, cross-dimensional
|
||||
consistency, enum coverage, query edge cases, orderbook fingerprint ordering,
|
||||
predictive properties, and ScenarioFactory integration.
|
||||
"""
|
||||
import pytest
|
||||
from malkhut.training.asset_classification import (
|
||||
Sector, TokenRole, SupplyModel, ConsensusFamily, SmartContractCapability,
|
||||
MarketCapTier, DerivativeAccess, VolatilityProfile, LiquidityProfile,
|
||||
AssetProfile, ASSET_PROFILES,
|
||||
get_asset_profile, list_assets,
|
||||
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_liquidity, get_assets_by_derivatives, get_gas_tokens,
|
||||
get_pov_assets, get_shortable_assets,
|
||||
get_multi_sector_assets, get_multi_role_assets,
|
||||
)
|
||||
from malkhut.training.cma_trainer import ScenarioFactory
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Profile existence and structural integrity
|
||||
# ==============================================================================
|
||||
|
||||
class TestAssetProfileExists:
|
||||
def test_minimum_assets(self):
|
||||
assert len(ASSET_PROFILES) >= 13
|
||||
|
||||
def test_all_profiles_frozen(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
with pytest.raises(AttributeError):
|
||||
p.symbol = "X"
|
||||
|
||||
def test_symbol_matches_key(self):
|
||||
for symbol, profile in ASSET_PROFILES.items():
|
||||
assert profile.symbol == symbol
|
||||
|
||||
def test_sectors_are_tuples(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert isinstance(p.sectors, tuple)
|
||||
assert len(p.sectors) >= 1
|
||||
|
||||
def test_token_roles_are_tuples(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert isinstance(p.token_roles, tuple)
|
||||
assert len(p.token_roles) >= 1
|
||||
|
||||
def test_no_duplicate_symbols(self):
|
||||
assert len(ASSET_PROFILES) == len(set(ASSET_PROFILES.keys()))
|
||||
|
||||
def test_all_sectors_are_valid_sector_enum(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
for s in p.sectors:
|
||||
assert isinstance(s, Sector)
|
||||
|
||||
def test_all_token_roles_are_valid_enum(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
for r in p.token_roles:
|
||||
assert isinstance(r, TokenRole)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Enum coverage — every enum value used by at least one asset
|
||||
# ==============================================================================
|
||||
|
||||
class TestEnumCoverage:
|
||||
def test_all_primary_sectors_represented(self):
|
||||
"""Every 'primary' sector (first in any asset's tuple) must be covered."""
|
||||
used_primary = {p.sector for p in ASSET_PROFILES.values()}
|
||||
assert Sector.CURRENCY in used_primary
|
||||
assert Sector.LAYER1 in used_primary
|
||||
assert Sector.LAYER2 in used_primary
|
||||
assert Sector.DEFI in used_primary
|
||||
assert Sector.ORACLE in used_primary
|
||||
assert Sector.EXCHANGE in used_primary
|
||||
assert Sector.MEME in used_primary
|
||||
|
||||
def test_all_token_roles_represented(self):
|
||||
used = set()
|
||||
for p in ASSET_PROFILES.values():
|
||||
used.update(p.token_roles)
|
||||
for r in TokenRole:
|
||||
assert r in used, f"TokenRole.{r.value} not represented"
|
||||
|
||||
def test_all_supply_models_represented(self):
|
||||
used = {p.supply_model for p in ASSET_PROFILES.values()}
|
||||
for m in SupplyModel:
|
||||
assert m in used, f"SupplyModel.{m.value} not represented"
|
||||
|
||||
def test_all_consensus_families_represented(self):
|
||||
used = {p.consensus for p in ASSET_PROFILES.values()}
|
||||
for c in ConsensusFamily:
|
||||
assert c in used, f"ConsensusFamily.{c.value} not represented"
|
||||
|
||||
def test_all_smart_contract_capabilities_represented(self):
|
||||
used = {p.smart_contracts for p in ASSET_PROFILES.values()}
|
||||
for sc in SmartContractCapability:
|
||||
assert sc in used, f"SmartContractCapability.{sc.value} not represented"
|
||||
|
||||
def test_all_common_market_cap_tiers_represented(self):
|
||||
used = {p.market_cap_tier for p in ASSET_PROFILES.values()}
|
||||
assert MarketCapTier.MEGA in used
|
||||
assert MarketCapTier.LARGE in used
|
||||
assert MarketCapTier.MID in used
|
||||
assert MarketCapTier.SMALL in used
|
||||
|
||||
def test_all_common_volatility_profiles_represented(self):
|
||||
used = {p.volatility_profile for p in ASSET_PROFILES.values()}
|
||||
assert VolatilityProfile.LOW in used
|
||||
assert VolatilityProfile.MEDIUM in used
|
||||
assert VolatilityProfile.HIGH in used
|
||||
|
||||
def test_all_common_liquidity_profiles_represented(self):
|
||||
used = {p.liquidity_profile for p in ASSET_PROFILES.values()}
|
||||
assert LiquidityProfile.DEEP in used
|
||||
assert LiquidityProfile.NORMAL in used
|
||||
assert LiquidityProfile.THIN in used
|
||||
|
||||
def test_common_derivative_access_represented(self):
|
||||
used = {p.derivative_access for p in ASSET_PROFILES.values()}
|
||||
assert DerivativeAccess.PERPS_AND_OPTIONS in used
|
||||
assert DerivativeAccess.PERPS_ONLY in used
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Multi-label overlap
|
||||
# ==============================================================================
|
||||
|
||||
class TestMultiLabelOverlap:
|
||||
def test_eth_multiple_sectors(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert Sector.LAYER1 in p.sectors
|
||||
assert Sector.DEFI in p.sectors
|
||||
assert len(p.sectors) == 2
|
||||
|
||||
def test_eth_multiple_roles(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
assert TokenRole.STORE_OF_VALUE in p.token_roles
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
assert len(p.token_roles) == 3
|
||||
|
||||
def test_bnb_multiple_sectors(self):
|
||||
p = get_asset_profile("BNBUSDT")
|
||||
assert Sector.EXCHANGE in p.sectors
|
||||
assert Sector.LAYER1 in p.sectors
|
||||
assert len(p.sectors) == 2
|
||||
|
||||
def test_bnb_multiple_roles(self):
|
||||
p = get_asset_profile("BNBUSDT")
|
||||
assert TokenRole.EXCHANGE_FEE in p.token_roles
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
|
||||
def test_doge_multi_sector(self):
|
||||
p = get_asset_profile("DOGEUSDT")
|
||||
assert Sector.MEME in p.sectors
|
||||
assert Sector.CURRENCY in p.sectors
|
||||
|
||||
def test_doge_multi_role(self):
|
||||
p = get_asset_profile("DOGEUSDT")
|
||||
assert TokenRole.MEME in p.token_roles
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
|
||||
def test_uni_multi_role(self):
|
||||
p = get_asset_profile("UNIUSDT")
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
assert TokenRole.UTILITY in p.token_roles
|
||||
|
||||
def test_dot_multi_role(self):
|
||||
p = get_asset_profile("DOTUSDT")
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
|
||||
def test_avax_multi_sector(self):
|
||||
p = get_asset_profile("AVAXUSDT")
|
||||
assert Sector.LAYER1 in p.sectors
|
||||
assert Sector.DEFI in p.sectors
|
||||
|
||||
def test_btc_single_sector(self):
|
||||
p = get_asset_profile("BTCUSDT")
|
||||
assert len(p.sectors) == 1
|
||||
assert Sector.CURRENCY in p.sectors
|
||||
|
||||
def test_sol_single_sector_and_role(self):
|
||||
p = get_asset_profile("SOLUSDT")
|
||||
assert len(p.sectors) == 1
|
||||
assert len(p.token_roles) == 1
|
||||
|
||||
def test_ada_single_sector_and_role(self):
|
||||
p = get_asset_profile("ADAUSDT")
|
||||
assert len(p.sectors) == 1
|
||||
assert len(p.token_roles) == 1
|
||||
|
||||
def test_atom_single_sector_and_role(self):
|
||||
p = get_asset_profile("ATOMUSDT")
|
||||
assert len(p.sectors) == 1
|
||||
assert len(p.token_roles) == 1
|
||||
|
||||
def test_multi_sector_assets_count(self):
|
||||
multi = get_multi_sector_assets()
|
||||
symbols = {p.symbol for p in multi}
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
assert "DOGEUSDT" in symbols
|
||||
assert "AVAXUSDT" in symbols
|
||||
assert len(multi) == 4
|
||||
|
||||
def test_multi_role_assets_count(self):
|
||||
multi = get_multi_role_assets()
|
||||
symbols = {p.symbol for p in multi}
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
assert "DOGEUSDT" in symbols
|
||||
assert "UNIUSDT" in symbols
|
||||
assert "DOTUSDT" in symbols
|
||||
assert len(multi) == 5
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Backward-compatible primary labels
|
||||
# ==============================================================================
|
||||
|
||||
class TestBackwardCompatiblePrimaryLabels:
|
||||
def test_eth_primary_sector(self):
|
||||
assert get_asset_profile("ETHUSDT").sector == Sector.LAYER1
|
||||
|
||||
def test_bnb_primary_sector(self):
|
||||
assert get_asset_profile("BNBUSDT").sector == Sector.EXCHANGE
|
||||
|
||||
def test_doge_primary_sector(self):
|
||||
assert get_asset_profile("DOGEUSDT").sector == Sector.MEME
|
||||
|
||||
def test_eth_primary_role(self):
|
||||
assert get_asset_profile("ETHUSDT").token_role == TokenRole.GAS
|
||||
|
||||
def test_bnb_primary_role(self):
|
||||
assert get_asset_profile("BNBUSDT").token_role == TokenRole.EXCHANGE_FEE
|
||||
|
||||
def test_uni_primary_role(self):
|
||||
assert get_asset_profile("UNIUSDT").token_role == TokenRole.GOVERNANCE
|
||||
|
||||
def test_primary_matches_first_in_sectors(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.sector == p.sectors[0]
|
||||
|
||||
def test_primary_matches_first_in_roles(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.token_role == p.token_roles[0]
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Per-asset profiles — exhaustive for all 13
|
||||
# ==============================================================================
|
||||
|
||||
class TestBTCProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("BTCUSDT").sector == Sector.CURRENCY
|
||||
|
||||
def test_token_role(self):
|
||||
assert get_asset_profile("BTCUSDT").token_role == TokenRole.STORE_OF_VALUE
|
||||
|
||||
def test_supply_model(self):
|
||||
assert get_asset_profile("BTCUSDT").supply_model == SupplyModel.FIXED_CAP
|
||||
|
||||
def test_consensus(self):
|
||||
assert get_asset_profile("BTCUSDT").consensus == ConsensusFamily.POW
|
||||
|
||||
def test_smart_contracts(self):
|
||||
assert get_asset_profile("BTCUSDT").smart_contracts == SmartContractCapability.NONE
|
||||
|
||||
def test_market_cap(self):
|
||||
assert get_asset_profile("BTCUSDT").market_cap_tier == MarketCapTier.MEGA
|
||||
|
||||
def test_volatility(self):
|
||||
assert get_asset_profile("BTCUSDT").volatility_profile == VolatilityProfile.LOW
|
||||
|
||||
def test_liquidity(self):
|
||||
assert get_asset_profile("BTCUSDT").liquidity_profile == LiquidityProfile.DEEP
|
||||
|
||||
def test_derivatives(self):
|
||||
p = get_asset_profile("BTCUSDT")
|
||||
assert p.derivative_access == DerivativeAccess.PERPS_AND_OPTIONS
|
||||
assert p.has_options is True
|
||||
assert p.has_funding is True
|
||||
|
||||
def test_tick_lot(self):
|
||||
p = get_asset_profile("BTCUSDT")
|
||||
assert p.tick_size == 0.1
|
||||
assert p.lot_size == 0.001
|
||||
assert p.price_decimals == 1
|
||||
|
||||
def test_fees(self):
|
||||
p = get_asset_profile("BTCUSDT")
|
||||
assert p.maker_fee_bps == -0.2
|
||||
assert p.taker_fee_bps == 0.5
|
||||
|
||||
def test_orderbook(self):
|
||||
p = get_asset_profile("BTCUSDT")
|
||||
assert p.typical_spread_bps == 0.3
|
||||
assert p.typical_depth_usd == 5_000_000
|
||||
assert p.typical_daily_volume_usd == 30_000_000_000
|
||||
|
||||
|
||||
class TestETHProfile:
|
||||
def test_sectors(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert p.sector == Sector.LAYER1
|
||||
assert Sector.DEFI in p.sectors
|
||||
|
||||
def test_roles(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert p.token_role == TokenRole.GAS
|
||||
assert TokenRole.STORE_OF_VALUE in p.token_roles
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
|
||||
def test_supply(self):
|
||||
assert get_asset_profile("ETHUSDT").supply_model == SupplyModel.DISINFLATIONARY
|
||||
|
||||
def test_consensus(self):
|
||||
assert get_asset_profile("ETHUSDT").consensus == ConsensusFamily.POS
|
||||
|
||||
def test_smart_contracts(self):
|
||||
assert get_asset_profile("ETHUSDT").smart_contracts == SmartContractCapability.FULL
|
||||
|
||||
def test_tick_lot(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert p.tick_size == 0.01
|
||||
assert p.lot_size == 0.001
|
||||
assert p.price_decimals == 2
|
||||
|
||||
def test_options(self):
|
||||
p = get_asset_profile("ETHUSDT")
|
||||
assert p.has_options is True
|
||||
assert p.has_funding is True
|
||||
|
||||
def test_is_gas(self):
|
||||
assert get_asset_profile("ETHUSDT").is_gas_token
|
||||
|
||||
|
||||
class TestSOLProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("SOLUSDT").sector == Sector.LAYER1
|
||||
|
||||
def test_supply(self):
|
||||
assert get_asset_profile("SOLUSDT").supply_model == SupplyModel.INFLATIONARY
|
||||
|
||||
def test_consensus(self):
|
||||
assert get_asset_profile("SOLUSDT").consensus == ConsensusFamily.POS
|
||||
|
||||
def test_volatility(self):
|
||||
assert get_asset_profile("SOLUSDT").volatility_profile == VolatilityProfile.HIGH
|
||||
|
||||
def test_no_options(self):
|
||||
assert get_asset_profile("SOLUSDT").has_options is False
|
||||
|
||||
def test_lot_size(self):
|
||||
assert get_asset_profile("SOLUSDT").lot_size == 0.01
|
||||
|
||||
|
||||
class TestDOGEProfile:
|
||||
def test_sectors(self):
|
||||
p = get_asset_profile("DOGEUSDT")
|
||||
assert Sector.MEME in p.sectors
|
||||
assert Sector.CURRENCY in p.sectors
|
||||
|
||||
def test_roles(self):
|
||||
p = get_asset_profile("DOGEUSDT")
|
||||
assert TokenRole.MEME in p.token_roles
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
|
||||
def test_pow_forced_selling(self):
|
||||
assert get_asset_profile("DOGEUSDT").supply_pressure == "forced"
|
||||
|
||||
def test_no_smart_contracts(self):
|
||||
assert get_asset_profile("DOGEUSDT").smart_contracts == SmartContractCapability.NONE
|
||||
|
||||
def test_five_decimal_tick(self):
|
||||
p = get_asset_profile("DOGEUSDT")
|
||||
assert p.tick_size == 0.00001
|
||||
assert p.price_decimals == 5
|
||||
|
||||
def test_one_unit_lot(self):
|
||||
assert get_asset_profile("DOGEUSDT").lot_size == 1.0
|
||||
|
||||
|
||||
class TestADAProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("ADAUSDT").sector == Sector.LAYER1
|
||||
|
||||
def test_consensus(self):
|
||||
assert get_asset_profile("ADAUSDT").consensus == ConsensusFamily.DPOS
|
||||
|
||||
def test_four_decimal_tick(self):
|
||||
p = get_asset_profile("ADAUSDT")
|
||||
assert p.tick_size == 0.0001
|
||||
assert p.price_decimals == 4
|
||||
|
||||
|
||||
class TestAVAXProfile:
|
||||
def test_sectors(self):
|
||||
p = get_asset_profile("AVAXUSDT")
|
||||
assert Sector.LAYER1 in p.sectors
|
||||
assert Sector.DEFI in p.sectors
|
||||
|
||||
def test_supply(self):
|
||||
assert get_asset_profile("AVAXUSDT").supply_model == SupplyModel.INFLATIONARY
|
||||
|
||||
|
||||
class TestUNIProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("UNIUSDT").sector == Sector.DEFI
|
||||
|
||||
def test_roles(self):
|
||||
p = get_asset_profile("UNIUSDT")
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
assert TokenRole.UTILITY in p.token_roles
|
||||
|
||||
def test_small_cap(self):
|
||||
assert get_asset_profile("UNIUSDT").market_cap_tier == MarketCapTier.SMALL
|
||||
|
||||
|
||||
class TestLINKProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("LINKUSDT").sector == Sector.ORACLE
|
||||
|
||||
def test_role(self):
|
||||
assert get_asset_profile("LINKUSDT").token_role == TokenRole.UTILITY
|
||||
|
||||
def test_no_gas_role(self):
|
||||
assert get_asset_profile("LINKUSDT").is_gas_token is False
|
||||
|
||||
|
||||
class TestBNBProfile:
|
||||
def test_sectors(self):
|
||||
p = get_asset_profile("BNBUSDT")
|
||||
assert Sector.EXCHANGE in p.sectors
|
||||
assert Sector.LAYER1 in p.sectors
|
||||
|
||||
def test_roles(self):
|
||||
p = get_asset_profile("BNBUSDT")
|
||||
assert TokenRole.EXCHANGE_FEE in p.token_roles
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
|
||||
def test_burn_mechanism(self):
|
||||
assert get_asset_profile("BNBUSDT").supply_model == SupplyModel.BURN_MECHANISM
|
||||
|
||||
def test_partial_smart_contracts(self):
|
||||
assert get_asset_profile("BNBUSDT").smart_contracts == SmartContractCapability.PARTIAL
|
||||
|
||||
def test_lower_fees(self):
|
||||
p = get_asset_profile("BNBUSDT")
|
||||
assert p.maker_fee_bps == -0.1
|
||||
assert p.taker_fee_bps == 0.4
|
||||
|
||||
def test_is_gas(self):
|
||||
assert get_asset_profile("BNBUSDT").is_gas_token
|
||||
|
||||
|
||||
class TestMATICProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("MATICUSDT").sector == Sector.LAYER2
|
||||
|
||||
def test_four_decimal_tick(self):
|
||||
p = get_asset_profile("MATICUSDT")
|
||||
assert p.tick_size == 0.0001
|
||||
assert p.price_decimals == 4
|
||||
|
||||
|
||||
class TestAAVEProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("AAVEUSDT").sector == Sector.DEFI
|
||||
|
||||
def test_role(self):
|
||||
assert get_asset_profile("AAVEUSDT").token_role == TokenRole.GOVERNANCE
|
||||
|
||||
def test_fixed_cap(self):
|
||||
assert get_asset_profile("AAVEUSDT").supply_model == SupplyModel.FIXED_CAP
|
||||
|
||||
|
||||
class TestDOTProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("DOTUSDT").sector == Sector.LAYER1
|
||||
|
||||
def test_roles(self):
|
||||
p = get_asset_profile("DOTUSDT")
|
||||
assert TokenRole.GAS in p.token_roles
|
||||
assert TokenRole.GOVERNANCE in p.token_roles
|
||||
|
||||
def test_three_decimal_tick(self):
|
||||
p = get_asset_profile("DOTUSDT")
|
||||
assert p.tick_size == 0.001
|
||||
assert p.price_decimals == 3
|
||||
|
||||
|
||||
class TestATOMProfile:
|
||||
def test_sector(self):
|
||||
assert get_asset_profile("ATOMUSDT").sector == Sector.LAYER1
|
||||
|
||||
def test_supply(self):
|
||||
assert get_asset_profile("ATOMUSDT").supply_model == SupplyModel.INFLATIONARY
|
||||
|
||||
def test_consensus(self):
|
||||
assert get_asset_profile("ATOMUSDT").consensus == ConsensusFamily.POS
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Cross-dimensional consistency checks
|
||||
# ==============================================================================
|
||||
|
||||
class TestCrossDimensionalConsistency:
|
||||
def test_pow_assets_are_btc_doge_only(self):
|
||||
pows = get_assets_by_consensus(ConsensusFamily.POW)
|
||||
symbols = {p.symbol for p in pows}
|
||||
assert symbols == {"BTCUSDT", "DOGEUSDT"}
|
||||
|
||||
def test_defi_sector_has_full_smart_contracts(self):
|
||||
for p in get_assets_by_sector(Sector.DEFI):
|
||||
assert p.smart_contracts == SmartContractCapability.FULL, \
|
||||
f"{p.symbol} is DEFI but not FULL smart contracts"
|
||||
|
||||
def test_l1_native_assets_have_full_smart_contracts(self):
|
||||
l1_native = [p for p in get_assets_by_sector(Sector.LAYER1)
|
||||
if Sector.LAYER1 in p.sectors and Sector.EXCHANGE not in p.sectors]
|
||||
for p in l1_native:
|
||||
assert p.smart_contracts == SmartContractCapability.FULL, \
|
||||
f"{p.symbol} is LAYER1 but not FULL smart contracts"
|
||||
|
||||
def test_mega_cap_is_btc_only(self):
|
||||
mega = get_assets_by_market_cap(MarketCapTier.MEGA)
|
||||
assert len(mega) == 1
|
||||
assert mega[0].symbol == "BTCUSDT"
|
||||
|
||||
def test_btc_only_has_options(self):
|
||||
with_options = [p for p in ASSET_PROFILES.values() if p.has_options]
|
||||
assert len(with_options) == 2
|
||||
symbols = {p.symbol for p in with_options}
|
||||
assert symbols == {"BTCUSDT", "ETHUSDT"}
|
||||
|
||||
def test_only_two_perps_and_options(self):
|
||||
pao = get_assets_by_derivatives(DerivativeAccess.PERPS_AND_OPTIONS)
|
||||
assert len(pao) == 2
|
||||
symbols = {p.symbol for p in pao}
|
||||
assert symbols == {"BTCUSDT", "ETHUSDT"}
|
||||
|
||||
def test_all_have_funding(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.has_funding is True
|
||||
|
||||
def test_only_bnb_has_burn_mechanism(self):
|
||||
burned = get_assets_by_supply(SupplyModel.BURN_MECHANISM)
|
||||
assert len(burned) == 1
|
||||
assert burned[0].symbol == "BNBUSDT"
|
||||
|
||||
def test_only_bnb_has_partial_smart_contracts(self):
|
||||
partial = [p for p in ASSET_PROFILES.values()
|
||||
if p.smart_contracts == SmartContractCapability.PARTIAL]
|
||||
assert len(partial) == 1
|
||||
assert partial[0].symbol == "BNBUSDT"
|
||||
|
||||
def test_no_smart_contracts_are_btc_doge(self):
|
||||
none_sc = [p for p in ASSET_PROFILES.values()
|
||||
if p.smart_contracts == SmartContractCapability.NONE]
|
||||
symbols = {p.symbol for p in none_sc}
|
||||
assert symbols == {"BTCUSDT", "DOGEUSDT"}
|
||||
|
||||
def test_only_btc_is_mega(self):
|
||||
mega = get_assets_by_market_cap(MarketCapTier.MEGA)
|
||||
assert len(mega) == 1
|
||||
|
||||
def test_large_cap_are_eth_bnb(self):
|
||||
large = get_assets_by_market_cap(MarketCapTier.LARGE)
|
||||
symbols = {p.symbol for p in large}
|
||||
assert symbols == {"ETHUSDT", "BNBUSDT"}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Predictive properties
|
||||
# ==============================================================================
|
||||
|
||||
class TestSupplyPressure:
|
||||
def test_pow_forced(self):
|
||||
for symbol in ("BTCUSDT", "DOGEUSDT"):
|
||||
assert get_asset_profile(symbol).supply_pressure == "forced"
|
||||
|
||||
def test_pos_optional(self):
|
||||
pos_symbols = ["ETHUSDT", "SOLUSDT", "ADAUSDT", "AVAXUSDT",
|
||||
"UNIUSDT", "LINKUSDT", "AAVEUSDT", "DOTUSDT", "ATOMUSDT"]
|
||||
for symbol in pos_symbols:
|
||||
assert get_asset_profile(symbol).supply_pressure == "optional"
|
||||
|
||||
def test_dpos_optional(self):
|
||||
for symbol in ("ADAUSDT", "BNBUSDT", "DOTUSDT"):
|
||||
assert get_asset_profile(symbol).supply_pressure == "optional"
|
||||
|
||||
def test_pov_only_pow(self):
|
||||
pov = get_pov_assets()
|
||||
assert len(pov) == 2
|
||||
for p in pov:
|
||||
assert p.consensus == ConsensusFamily.POW
|
||||
|
||||
|
||||
class TestDemandElasticity:
|
||||
def test_gas_inelastic(self):
|
||||
for p in get_gas_tokens():
|
||||
assert p.demand_elasticity == "inelastic"
|
||||
|
||||
def test_store_of_value_inelastic(self):
|
||||
sov = get_assets_by_token_role(TokenRole.STORE_OF_VALUE)
|
||||
for p in sov:
|
||||
assert p.demand_elasticity == "inelastic"
|
||||
|
||||
def test_elastic_tokens(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
if p.demand_elasticity == "elastic":
|
||||
assert TokenRole.GAS not in p.token_roles
|
||||
assert TokenRole.STORE_OF_VALUE not in p.token_roles
|
||||
|
||||
def test_elastic_count(self):
|
||||
elastic = [p for p in ASSET_PROFILES.values()
|
||||
if p.demand_elasticity == "elastic"]
|
||||
assert len(elastic) >= 3
|
||||
|
||||
def test_inelastic_count(self):
|
||||
inelastic = [p for p in ASSET_PROFILES.values()
|
||||
if p.demand_elasticity == "inelastic"]
|
||||
assert len(inelastic) >= 8
|
||||
|
||||
def test_all_assets_are_one_or_other(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.demand_elasticity in ("inelastic", "elastic")
|
||||
|
||||
|
||||
class TestIsGasToken:
|
||||
def test_gas_tokens(self):
|
||||
gas = get_gas_tokens()
|
||||
symbols = {p.symbol for p in gas}
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "SOLUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
assert "DOGEUSDT" in symbols
|
||||
assert "MATICUSDT" in symbols
|
||||
assert "DOTUSDT" in symbols
|
||||
|
||||
def test_non_gas_tokens(self):
|
||||
assert get_asset_profile("BTCUSDT").is_gas_token is False
|
||||
assert get_asset_profile("UNIUSDT").is_gas_token is False
|
||||
assert get_asset_profile("AAVEUSDT").is_gas_token is False
|
||||
assert get_asset_profile("LINKUSDT").is_gas_token is False
|
||||
|
||||
|
||||
class TestIsPureCurrency:
|
||||
def test_currency_assets(self):
|
||||
assert get_asset_profile("BTCUSDT").is_pure_currency
|
||||
assert get_asset_profile("DOGEUSDT").is_pure_currency
|
||||
|
||||
def test_non_currency_assets(self):
|
||||
assert get_asset_profile("ETHUSDT").is_pure_currency is False
|
||||
assert get_asset_profile("SOLUSDT").is_pure_currency is False
|
||||
assert get_asset_profile("UNIUSDT").is_pure_currency is False
|
||||
|
||||
|
||||
class TestCanBeShorted:
|
||||
def test_all_shortable(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.can_be_shorted
|
||||
|
||||
def test_shortable_matches_derivatives(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
if p.derivative_access == DerivativeAccess.NONE:
|
||||
assert not p.can_be_shorted
|
||||
else:
|
||||
assert p.can_be_shorted
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Orderbook fingerprint ordering
|
||||
# ==============================================================================
|
||||
|
||||
class TestOrderbookFingerprintOrdering:
|
||||
def test_spread_increases_with_risk(self):
|
||||
btc_spread = get_asset_profile("BTCUSDT").typical_spread_bps
|
||||
eth_spread = get_asset_profile("ETHUSDT").typical_spread_bps
|
||||
sol_spread = get_asset_profile("SOLUSDT").typical_spread_bps
|
||||
doge_spread = get_asset_profile("DOGEUSDT").typical_spread_bps
|
||||
assert btc_spread < eth_spread < sol_spread < doge_spread
|
||||
|
||||
def test_depth_decreases_with_cap(self):
|
||||
btc_depth = get_asset_profile("BTCUSDT").typical_depth_usd
|
||||
eth_depth = get_asset_profile("ETHUSDT").typical_depth_usd
|
||||
sol_depth = get_asset_profile("SOLUSDT").typical_depth_usd
|
||||
aave_depth = get_asset_profile("AAVEUSDT").typical_depth_usd
|
||||
assert btc_depth > eth_depth > sol_depth > aave_depth
|
||||
|
||||
def test_volume_decreases_with_cap(self):
|
||||
btc_vol = get_asset_profile("BTCUSDT").typical_daily_volume_usd
|
||||
eth_vol = get_asset_profile("ETHUSDT").typical_daily_volume_usd
|
||||
assert btc_vol > eth_vol
|
||||
|
||||
def test_deep_has_highest_depth(self):
|
||||
for p in get_assets_by_liquidity(LiquidityProfile.DEEP):
|
||||
assert p.typical_depth_usd >= 2_000_000
|
||||
|
||||
def test_thin_has_lowest_depth(self):
|
||||
for p in get_assets_by_liquidity(LiquidityProfile.THIN):
|
||||
assert p.typical_depth_usd <= 700_000
|
||||
|
||||
def test_spread_nonzero(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.typical_spread_bps > 0
|
||||
|
||||
def test_volume_nonzero(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.typical_daily_volume_usd > 0
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Profile field validation
|
||||
# ==============================================================================
|
||||
|
||||
class TestProfileFieldValidation:
|
||||
def test_tick_size_positive(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.tick_size > 0
|
||||
|
||||
def test_lot_size_positive(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.lot_size > 0
|
||||
|
||||
def test_price_decimals_non_negative(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.price_decimals >= 0
|
||||
|
||||
def test_fee_bps_reasonable(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert -1.0 <= p.maker_fee_bps <= 1.0
|
||||
assert 0.0 <= p.taker_fee_bps <= 2.0
|
||||
assert p.taker_fee_bps >= p.maker_fee_bps
|
||||
|
||||
def test_depth_usd_positive(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.typical_depth_usd > 0
|
||||
|
||||
def test_volume_usd_positive(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.typical_daily_volume_usd > 0
|
||||
|
||||
def test_volume_exceeds_depth(self):
|
||||
for p in ASSET_PROFILES.values():
|
||||
assert p.typical_daily_volume_usd > p.typical_depth_usd
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Query functions — exhaustive with edge cases
|
||||
# ==============================================================================
|
||||
|
||||
class TestQueryFunctions:
|
||||
def test_get_asset_profile_hit(self):
|
||||
assert get_asset_profile("SOLUSDT") is not None
|
||||
|
||||
def test_get_asset_profile_miss(self):
|
||||
assert get_asset_profile("FAKEUSDT") is None
|
||||
assert get_asset_profile("") is None
|
||||
assert get_asset_profile("BTCUSDTx") is None
|
||||
|
||||
def test_list_assets_count(self):
|
||||
assert len(list_assets()) == 13
|
||||
|
||||
def test_list_assets_all_present(self):
|
||||
expected = {"BTCUSDT", "ETHUSDT", "SOLUSDT", "DOGEUSDT", "ADAUSDT",
|
||||
"AVAXUSDT", "UNIUSDT", "LINKUSDT", "BNBUSDT", "MATICUSDT",
|
||||
"AAVEUSDT", "DOTUSDT", "ATOMUSDT"}
|
||||
assert set(list_assets()) == expected
|
||||
|
||||
def test_by_sector_layer1(self):
|
||||
l1 = get_assets_by_sector(Sector.LAYER1)
|
||||
symbols = {p.symbol for p in l1}
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "SOLUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
|
||||
def test_by_sector_defi(self):
|
||||
defi = get_assets_by_sector(Sector.DEFI)
|
||||
symbols = {p.symbol for p in defi}
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "UNIUSDT" in symbols
|
||||
assert "AAVEUSDT" in symbols
|
||||
assert "AVAXUSDT" in symbols
|
||||
|
||||
def test_by_sector_exchange(self):
|
||||
ex = get_assets_by_sector(Sector.EXCHANGE)
|
||||
assert len(ex) == 1
|
||||
assert ex[0].symbol == "BNBUSDT"
|
||||
|
||||
def test_by_sector_oracle(self):
|
||||
orc = get_assets_by_sector(Sector.ORACLE)
|
||||
assert len(orc) == 1
|
||||
assert orc[0].symbol == "LINKUSDT"
|
||||
|
||||
def test_by_sector_layer2(self):
|
||||
l2 = get_assets_by_sector(Sector.LAYER2)
|
||||
assert len(l2) == 1
|
||||
assert l2[0].symbol == "MATICUSDT"
|
||||
|
||||
def test_by_sector_meme(self):
|
||||
meme = get_assets_by_sector(Sector.MEME)
|
||||
symbols = {p.symbol for p in meme}
|
||||
assert "DOGEUSDT" in symbols
|
||||
|
||||
def test_by_sector_storage_empty(self):
|
||||
assert get_assets_by_sector(Sector.STORAGE) == []
|
||||
|
||||
def test_by_sector_privacy_empty(self):
|
||||
assert get_assets_by_sector(Sector.PRIVACY) == []
|
||||
|
||||
def test_by_sector_gaming_empty(self):
|
||||
assert get_assets_by_sector(Sector.GAMING_NFT) == []
|
||||
|
||||
def test_by_token_role_gas(self):
|
||||
gas = get_assets_by_token_role(TokenRole.GAS)
|
||||
assert len(gas) >= 7
|
||||
|
||||
def test_by_token_role_store_of_value(self):
|
||||
sov = get_assets_by_token_role(TokenRole.STORE_OF_VALUE)
|
||||
symbols = {p.symbol for p in sov}
|
||||
assert "BTCUSDT" in symbols
|
||||
assert "ETHUSDT" in symbols
|
||||
|
||||
def test_by_token_role_governance(self):
|
||||
gov = get_assets_by_token_role(TokenRole.GOVERNANCE)
|
||||
symbols = {p.symbol for p in gov}
|
||||
assert "UNIUSDT" in symbols
|
||||
assert "AAVEUSDT" in symbols
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "DOTUSDT" in symbols
|
||||
|
||||
def test_by_token_role_utility(self):
|
||||
util = get_assets_by_token_role(TokenRole.UTILITY)
|
||||
symbols = {p.symbol for p in util}
|
||||
assert "LINKUSDT" in symbols
|
||||
assert "UNIUSDT" in symbols
|
||||
|
||||
def test_by_token_role_exchange_fee(self):
|
||||
ef = get_assets_by_token_role(TokenRole.EXCHANGE_FEE)
|
||||
assert len(ef) == 1
|
||||
assert ef[0].symbol == "BNBUSDT"
|
||||
|
||||
def test_by_token_role_meme(self):
|
||||
meme = get_assets_by_token_role(TokenRole.MEME)
|
||||
symbols = {p.symbol for p in meme}
|
||||
assert "DOGEUSDT" in symbols
|
||||
|
||||
def test_by_supply_fixed_cap(self):
|
||||
fixed = get_assets_by_supply(SupplyModel.FIXED_CAP)
|
||||
symbols = {p.symbol for p in fixed}
|
||||
assert "BTCUSDT" in symbols
|
||||
assert "AAVEUSDT" in symbols
|
||||
|
||||
def test_by_supply_disinflationary(self):
|
||||
dis = get_assets_by_supply(SupplyModel.DISINFLATIONARY)
|
||||
assert len(dis) == 1
|
||||
assert dis[0].symbol == "ETHUSDT"
|
||||
|
||||
def test_by_supply_burn(self):
|
||||
burn = get_assets_by_supply(SupplyModel.BURN_MECHANISM)
|
||||
assert len(burn) == 1
|
||||
assert burn[0].symbol == "BNBUSDT"
|
||||
|
||||
def test_by_consensus_pow(self):
|
||||
pows = get_assets_by_consensus(ConsensusFamily.POW)
|
||||
symbols = {p.symbol for p in pows}
|
||||
assert symbols == {"BTCUSDT", "DOGEUSDT"}
|
||||
|
||||
def test_by_consensus_dpos(self):
|
||||
dpos = get_assets_by_consensus(ConsensusFamily.DPOS)
|
||||
symbols = {p.symbol for p in dpos}
|
||||
assert "ADAUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
assert "DOTUSDT" in symbols
|
||||
|
||||
def test_by_market_cap_mega(self):
|
||||
mega = get_assets_by_market_cap(MarketCapTier.MEGA)
|
||||
assert len(mega) == 1
|
||||
|
||||
def test_by_market_cap_large(self):
|
||||
large = get_assets_by_market_cap(MarketCapTier.LARGE)
|
||||
symbols = {p.symbol for p in large}
|
||||
assert symbols == {"ETHUSDT", "BNBUSDT"}
|
||||
|
||||
def test_by_market_cap_small(self):
|
||||
small = get_assets_by_market_cap(MarketCapTier.SMALL)
|
||||
assert len(small) >= 4
|
||||
|
||||
def test_by_volatility_low(self):
|
||||
low = get_assets_by_volatility(VolatilityProfile.LOW)
|
||||
assert len(low) == 1
|
||||
assert low[0].symbol == "BTCUSDT"
|
||||
|
||||
def test_by_volatility_medium(self):
|
||||
med = get_assets_by_volatility(VolatilityProfile.MEDIUM)
|
||||
assert len(med) >= 3
|
||||
|
||||
def test_by_volatility_high(self):
|
||||
high = get_assets_by_volatility(VolatilityProfile.HIGH)
|
||||
assert len(high) >= 5
|
||||
|
||||
def test_by_volatility_extreme_empty(self):
|
||||
assert get_assets_by_volatility(VolatilityProfile.EXTREME) == []
|
||||
|
||||
def test_by_liquidity_deep(self):
|
||||
deep = get_assets_by_liquidity(LiquidityProfile.DEEP)
|
||||
symbols = {p.symbol for p in deep}
|
||||
assert "BTCUSDT" in symbols
|
||||
assert "ETHUSDT" in symbols
|
||||
assert "BNBUSDT" in symbols
|
||||
|
||||
def test_by_liquidity_normal(self):
|
||||
norm = get_assets_by_liquidity(LiquidityProfile.NORMAL)
|
||||
assert len(norm) >= 4
|
||||
|
||||
def test_by_liquidity_thin(self):
|
||||
thin = get_assets_by_liquidity(LiquidityProfile.THIN)
|
||||
assert len(thin) >= 4
|
||||
|
||||
def test_by_liquidity_illiquid_empty(self):
|
||||
assert get_assets_by_liquidity(LiquidityProfile.ILLIQUID) == []
|
||||
|
||||
def test_by_derivatives_perps_options(self):
|
||||
pao = get_assets_by_derivatives(DerivativeAccess.PERPS_AND_OPTIONS)
|
||||
assert len(pao) == 2
|
||||
|
||||
def test_by_derivatives_perps_only(self):
|
||||
po = get_assets_by_derivatives(DerivativeAccess.PERPS_ONLY)
|
||||
assert len(po) == 11
|
||||
|
||||
def test_by_derivatives_none_empty(self):
|
||||
assert get_assets_by_derivatives(DerivativeAccess.NONE) == []
|
||||
|
||||
def test_shortable_count(self):
|
||||
assert len(get_shortable_assets()) == 13
|
||||
|
||||
def test_get_gas_tokens_count(self):
|
||||
assert len(get_gas_tokens()) == 9
|
||||
|
||||
def test_get_pov_assets_count(self):
|
||||
assert len(get_pov_assets()) == 2
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ScenarioFactory integration
|
||||
# ==============================================================================
|
||||
|
||||
class TestMultiAssetScenarios:
|
||||
def test_multi_asset_suite_3_assets(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(
|
||||
symbols=("BTCUSDT", "ETHUSDT", "SOLUSDT"),
|
||||
steps_per_scenario=5,
|
||||
)
|
||||
assert len(suite) >= 90
|
||||
|
||||
def test_multi_asset_uses_profiles(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(symbols=("ETHUSDT",), steps_per_scenario=5)
|
||||
for s in suite:
|
||||
assert s.initial_state.venue.symbol == "ETHUSDT"
|
||||
assert s.initial_state.venue.tick_size == 0.01
|
||||
|
||||
def test_all_assets_have_scenarios(self):
|
||||
factory = ScenarioFactory()
|
||||
assets = list_assets()
|
||||
suite = factory.build_suite(symbols=tuple(assets), steps_per_scenario=3)
|
||||
symbols_in_suite = set(s.initial_state.venue.symbol for s in suite)
|
||||
for asset in assets:
|
||||
assert asset in symbols_in_suite
|
||||
|
||||
def test_btc_uses_btc_tick(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=3)
|
||||
for s in suite:
|
||||
assert s.initial_state.venue.tick_size == 0.1
|
||||
|
||||
def test_doge_uses_doge_tick(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(symbols=("DOGEUSDT",), steps_per_scenario=3)
|
||||
for s in suite:
|
||||
assert s.initial_state.venue.tick_size == 0.00001
|
||||
|
||||
def test_single_asset_scenario_count(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=5)
|
||||
assert len(suite) >= 30
|
||||
|
||||
def test_steps_per_scenario_honored(self):
|
||||
factory = ScenarioFactory()
|
||||
suite = factory.build_suite(symbols=("ETHUSDT",), steps_per_scenario=10)
|
||||
for s in suite:
|
||||
assert s.max_steps >= 10
|
||||
398
MALKHUT/malkhut/training/asset_behavior.py
Normal file
398
MALKHUT/malkhut/training/asset_behavior.py
Normal file
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
Asset Behavior DSL — Pure Python composable behavior definitions.
|
||||
|
||||
Decomposes asset behavior into orthogonal dimensions, each with
|
||||
empirically-validated parameters from live Binance/BingX API + academic
|
||||
literature (Bouchaud, Cont/Stoikov, Cartea/Jaimungal).
|
||||
|
||||
13 pre-defined assets, 3 composable templates, 10 orthogonal dimensions.
|
||||
Auto-compilable via asset_compiler.py for any Binance-listed symbol.
|
||||
|
||||
Usage:
|
||||
from malkhut.training.asset_behavior import get_behavior, ASSET_BEHAVIORS
|
||||
btc = get_behavior("BTCUSDT")
|
||||
print(btc.depth.amplitude_usd) # $750,000
|
||||
print(btc.expected_slippage_bps(100_000)) # ~2 bps
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from malkhut.training.asset_classification import Sector, TokenRole
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DepthProfile:
|
||||
amplitude_usd: float
|
||||
alpha: float
|
||||
fragility_factor: float
|
||||
depth_at_10bps_usd: float
|
||||
depth_at_100bps_usd: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SpreadProfile:
|
||||
normal_bps: float
|
||||
stress_multiplier: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FlowProfile:
|
||||
orders_per_sec_normal: float
|
||||
orders_per_sec_stress: float
|
||||
cancel_fill_ratio: float
|
||||
median_order_usd: float
|
||||
p99_order_usd: float
|
||||
avg_trade_usd: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VolatilityProfile:
|
||||
annualized_normal: float
|
||||
annualized_crisis: float
|
||||
garch_alpha: float
|
||||
garch_beta: float
|
||||
half_life_hours: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class IntradayProfile:
|
||||
peak_hour_utc: int
|
||||
trough_hour_utc: int
|
||||
ratio: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WeekendProfile:
|
||||
vol_mult: float
|
||||
volume_mult: float
|
||||
spread_mult: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CorrelationProfile:
|
||||
eth_beta: float
|
||||
btc_corr_normal: float
|
||||
btc_corr_crash: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MarketMakerProfile:
|
||||
max_inventory_usd: float
|
||||
skew_tolerance_bps: float
|
||||
pull_speed_ms: float
|
||||
margin_bps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiquidationProfile:
|
||||
oi_mcap_ratio: float
|
||||
trigger_pct: float
|
||||
speed: str
|
||||
recovery: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FundingProfile:
|
||||
mean_bps_8h: float
|
||||
std_bps_8h: float
|
||||
positive_pct: float
|
||||
basis_typical_bps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RetailProfile:
|
||||
ratio: float
|
||||
inst_gap: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BingxProfile:
|
||||
spread_mult: float
|
||||
depth_ratio: float
|
||||
latency_ms: float
|
||||
taker_fee_bps: float
|
||||
maker_fee_bps: float
|
||||
funding_lag_hours: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BehaviorTemplate:
|
||||
name: str
|
||||
depth: DepthProfile
|
||||
spread: SpreadProfile
|
||||
flow: FlowProfile
|
||||
vol: VolatilityProfile
|
||||
intraday: IntradayProfile
|
||||
weekend: WeekendProfile
|
||||
correlation: CorrelationProfile
|
||||
market_maker: MarketMakerProfile
|
||||
liquidation: LiquidationProfile
|
||||
funding: FundingProfile
|
||||
retail: RetailProfile
|
||||
bingx: BingxProfile
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AssetBehavior:
|
||||
symbol: str
|
||||
depth: DepthProfile
|
||||
spread: SpreadProfile
|
||||
flow: FlowProfile
|
||||
vol: VolatilityProfile
|
||||
intraday: IntradayProfile
|
||||
weekend: WeekendProfile
|
||||
correlation: CorrelationProfile
|
||||
market_maker: MarketMakerProfile
|
||||
liquidation: LiquidationProfile
|
||||
funding: FundingProfile
|
||||
retail: RetailProfile
|
||||
bingx: BingxProfile
|
||||
template_name: str = ""
|
||||
reference_price: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def from_template(cls, symbol: str, template: BehaviorTemplate,
|
||||
overrides: Optional[Dict[str, object]] = None,
|
||||
reference_price: float = 0.0) -> AssetBehavior:
|
||||
params = {
|
||||
"depth": template.depth, "spread": template.spread,
|
||||
"flow": template.flow, "vol": template.vol,
|
||||
"intraday": template.intraday, "weekend": template.weekend,
|
||||
"correlation": template.correlation, "market_maker": template.market_maker,
|
||||
"liquidation": template.liquidation, "funding": template.funding,
|
||||
"retail": template.retail, "bingx": template.bingx,
|
||||
}
|
||||
if overrides:
|
||||
for key, value in overrides.items():
|
||||
if key in params:
|
||||
if isinstance(value, dict):
|
||||
base = params[key]
|
||||
params[key] = type(base)(**{**base.__dict__, **value})
|
||||
else:
|
||||
params[key] = value
|
||||
return cls(symbol=symbol, template_name=template.name,
|
||||
reference_price=reference_price, **params)
|
||||
|
||||
def depth_at_bps(self, bps: float) -> float:
|
||||
"""D(d) = amplitude * d^(1-alpha)"""
|
||||
return self.depth.amplitude_usd * (bps ** (1.0 - self.depth.alpha))
|
||||
|
||||
def expected_slippage_bps(self, order_usd: float) -> float:
|
||||
"""Estimate slippage for a market order of given notional."""
|
||||
d = 1.0
|
||||
cumulative = 0.0
|
||||
while cumulative < order_usd and d < 1000:
|
||||
cumulative += self.depth.amplitude_usd * (d ** (-self.depth.alpha))
|
||||
d += 1.0
|
||||
return d if cumulative >= order_usd else 1000.0
|
||||
|
||||
|
||||
TEMPLATES: Dict[str, BehaviorTemplate] = {}
|
||||
|
||||
|
||||
def _t(name: str, **kwargs) -> BehaviorTemplate:
|
||||
t = BehaviorTemplate(name=name, **kwargs)
|
||||
TEMPLATES[name] = t
|
||||
return t
|
||||
|
||||
|
||||
INSTITUTIONAL_BLUE_CHIP = _t("institutional_blue_chip",
|
||||
depth=DepthProfile(1_500_000, 0.70, 0.10, 7_000_000, 20_000_000),
|
||||
spread=SpreadProfile(0.01, 50.0),
|
||||
flow=FlowProfile(300, 5000, 20.0, 643, 200_000, 5_000),
|
||||
vol=VolatilityProfile(35.0, 100.0, 0.10, 0.88, 48),
|
||||
intraday=IntradayProfile(15, 19, 7.4),
|
||||
weekend=WeekendProfile(0.65, 0.52, 1.20),
|
||||
correlation=CorrelationProfile(1.00, 1.00, 1.00),
|
||||
market_maker=MarketMakerProfile(10_000_000, 15, 3.0, 0.5),
|
||||
liquidation=LiquidationProfile(0.005, 6.5, "slow", "fast"),
|
||||
funding=FundingProfile(0.59, 0.22, 100.0, 4.0),
|
||||
retail=RetailProfile(0.35, 0.04),
|
||||
bingx=BingxProfile(12.6, 0.048, 100, 5.0, 2.0, 4))
|
||||
|
||||
MID_CAP_L1 = _t("mid_cap_l1",
|
||||
depth=DepthProfile(200_000, 0.85, 0.15, 800_000, 3_000_000),
|
||||
spread=SpreadProfile(1.5, 10.0),
|
||||
flow=FlowProfile(100, 1500, 10.0, 800, 150_000, 2_000),
|
||||
vol=VolatilityProfile(75.0, 150.0, 0.12, 0.85, 36),
|
||||
intraday=IntradayProfile(15, 11, 5.0),
|
||||
weekend=WeekendProfile(0.70, 0.55, 1.25),
|
||||
correlation=CorrelationProfile(0.85, 0.60, 0.90),
|
||||
market_maker=MarketMakerProfile(2_000_000, 25, 15.0, 1.0),
|
||||
liquidation=LiquidationProfile(0.015, 5.0, "medium", "medium"),
|
||||
funding=FundingProfile(0.10, 0.30, 60.0, 3.0),
|
||||
retail=RetailProfile(0.65, 0.50),
|
||||
bingx=BingxProfile(3.0, 0.30, 100, 5.0, 2.0, 4))
|
||||
|
||||
RETAIL_MEME = _t("retail_meme",
|
||||
depth=DepthProfile(25_000, 1.00, 0.30, 100_000, 2_500_000),
|
||||
spread=SpreadProfile(1.35, 15.0),
|
||||
flow=FlowProfile(80, 800, 8.0, 96, 52_000, 200),
|
||||
vol=VolatilityProfile(78.0, 200.0, 0.15, 0.82, 24),
|
||||
intraday=IntradayProfile(15, 10, 4.5),
|
||||
weekend=WeekendProfile(0.70, 0.50, 1.30),
|
||||
correlation=CorrelationProfile(0.83, 0.45, 0.80),
|
||||
market_maker=MarketMakerProfile(500_000, 40, 25.0, 2.0),
|
||||
liquidation=LiquidationProfile(0.014, 4.0, "fast", "slow"),
|
||||
funding=FundingProfile(0.39, 0.34, 80.0, 2.0),
|
||||
retail=RetailProfile(0.80, 0.31),
|
||||
bingx=BingxProfile(7.0, 1.27, 100, 5.0, 2.0, 4))
|
||||
|
||||
|
||||
ASSET_BEHAVIORS: Dict[str, AssetBehavior] = {}
|
||||
|
||||
|
||||
def _b(symbol: str, template_name: str, reference_price: float = 0.0,
|
||||
**overrides) -> AssetBehavior:
|
||||
tmpl = TEMPLATES[template_name]
|
||||
b = AssetBehavior.from_template(symbol, tmpl, overrides or None, reference_price=reference_price)
|
||||
ASSET_BEHAVIORS[symbol] = b
|
||||
return b
|
||||
|
||||
|
||||
BTC = _b("BTCUSDT", "institutional_blue_chip", reference_price=64000.0,
|
||||
depth=DepthProfile(750_000, 0.70, 0.10, 5_983_000, 20_000_000),
|
||||
vol=VolatilityProfile(35.0, 100.0, 0.10, 0.88, 48),
|
||||
bingx=BingxProfile(12.6, 0.048, 100, 5.0, 2.0, 4))
|
||||
|
||||
ETH = _b("ETHUSDT", "institutional_blue_chip", reference_price=1800.0,
|
||||
depth=DepthProfile(600_000, 0.75, 0.12, 2_095_000, 7_200_000),
|
||||
vol=VolatilityProfile(66.5, 130.0, 0.12, 0.86, 40),
|
||||
correlation=CorrelationProfile(1.00, 0.90, 0.97),
|
||||
liquidation=LiquidationProfile(0.019, 4.0, "medium", "medium"),
|
||||
bingx=BingxProfile(9.0, 9.06, 100, 5.0, 2.0, 4))
|
||||
|
||||
SOL = _b("SOLUSDT", "mid_cap_l1", reference_price=80.0,
|
||||
depth=DepthProfile(400_000, 0.85, 0.18, 954_000, 4_000_000),
|
||||
spread=SpreadProfile(1.26, 8.0),
|
||||
vol=VolatilityProfile(71.9, 150.0, 0.13, 0.84, 32),
|
||||
correlation=CorrelationProfile(0.88, 0.65, 0.92),
|
||||
retail=RetailProfile(0.72, 0.75),
|
||||
bingx=BingxProfile(1.7, 0.33, 80, 5.0, 2.0, 2))
|
||||
|
||||
DOGE = _b("DOGEUSDT", "retail_meme", reference_price=0.07,
|
||||
depth=DepthProfile(22_000, 1.00, 0.30, 432_000, 2_772_000),
|
||||
vol=VolatilityProfile(77.9, 200.0, 0.15, 0.82, 24),
|
||||
correlation=CorrelationProfile(0.83, 0.45, 0.80),
|
||||
bingx=BingxProfile(7.0, 1.27, 100, 5.0, 2.0, 4))
|
||||
|
||||
ADA = _b("ADAUSDT", "mid_cap_l1", reference_price=0.17,
|
||||
depth=DepthProfile(60_000, 0.90, 0.20, 110_000, 1_500_000),
|
||||
spread=SpreadProfile(5.95, 12.0),
|
||||
vol=VolatilityProfile(78.7, 160.0, 0.14, 0.83, 30),
|
||||
bingx=BingxProfile(8.0, 0.20, 120, 5.0, 2.0, 4))
|
||||
|
||||
AVAX = _b("AVAXUSDT", "mid_cap_l1", reference_price=7.0,
|
||||
depth=DepthProfile(55_000, 0.88, 0.18, 107_000, 1_200_000),
|
||||
spread=SpreadProfile(1.48, 10.0),
|
||||
vol=VolatilityProfile(80.1, 160.0, 0.13, 0.84, 32),
|
||||
correlation=CorrelationProfile(0.79, 0.55, 0.88),
|
||||
bingx=BingxProfile(4.0, 0.25, 110, 5.0, 2.0, 4))
|
||||
|
||||
UNI = _b("UNIUSDT", "mid_cap_l1", reference_price=3.6,
|
||||
depth=DepthProfile(30_000, 0.95, 0.25, 53_000, 800_000),
|
||||
spread=SpreadProfile(2.76, 12.0),
|
||||
vol=VolatilityProfile(95.3, 220.0, 0.16, 0.81, 22),
|
||||
correlation=CorrelationProfile(0.70, 0.50, 0.85),
|
||||
market_maker=MarketMakerProfile(300_000, 50, 30.0, 2.5),
|
||||
retail=RetailProfile(0.60, 0.25),
|
||||
bingx=BingxProfile(10.0, 0.15, 150, 5.0, 2.0, 6))
|
||||
|
||||
LINK = _b("LINKUSDT", "mid_cap_l1", reference_price=8.0,
|
||||
depth=DepthProfile(80_000, 0.87, 0.17, 129_000, 1_800_000),
|
||||
spread=SpreadProfile(1.25, 8.0),
|
||||
vol=VolatilityProfile(77.3, 155.0, 0.12, 0.85, 34),
|
||||
correlation=CorrelationProfile(0.88, 0.62, 0.91),
|
||||
bingx=BingxProfile(3.5, 0.28, 100, 5.0, 2.0, 4))
|
||||
|
||||
BNB = _b("BNBUSDT", "institutional_blue_chip", reference_price=575.0,
|
||||
depth=DepthProfile(500_000, 0.78, 0.12, 2_000_000, 8_000_000),
|
||||
spread=SpreadProfile(0.50, 15.0),
|
||||
vol=VolatilityProfile(50.0, 120.0, 0.11, 0.87, 42),
|
||||
funding=FundingProfile(0.40, 0.25, 90.0, 3.0),
|
||||
retail=RetailProfile(0.50, 0.20),
|
||||
bingx=BingxProfile(5.0, 0.50, 100, 5.0, 2.0, 4))
|
||||
|
||||
MATIC = _b("MATICUSDT", "mid_cap_l1", reference_price=0.5,
|
||||
depth=DepthProfile(35_000, 0.92, 0.22, 55_000, 900_000),
|
||||
spread=SpreadProfile(1.50, 10.0),
|
||||
vol=VolatilityProfile(82.0, 170.0, 0.14, 0.83, 28),
|
||||
correlation=CorrelationProfile(0.82, 0.58, 0.89),
|
||||
bingx=BingxProfile(5.0, 0.30, 110, 5.0, 2.0, 4))
|
||||
|
||||
AAVE = _b("AAVEUSDT", "mid_cap_l1", reference_price=100.0,
|
||||
depth=DepthProfile(20_000, 0.95, 0.25, 35_000, 600_000),
|
||||
spread=SpreadProfile(2.50, 12.0),
|
||||
vol=VolatilityProfile(85.0, 200.0, 0.15, 0.82, 26),
|
||||
correlation=CorrelationProfile(0.75, 0.52, 0.86),
|
||||
market_maker=MarketMakerProfile(200_000, 45, 28.0, 2.0),
|
||||
retail=RetailProfile(0.55, 0.20),
|
||||
bingx=BingxProfile(10.0, 0.12, 140, 5.0, 2.0, 6))
|
||||
|
||||
DOT = _b("DOTUSDT", "mid_cap_l1", reference_price=6.0,
|
||||
depth=DepthProfile(70_000, 0.88, 0.18, 120_000, 1_500_000),
|
||||
spread=SpreadProfile(1.00, 8.0),
|
||||
vol=VolatilityProfile(72.0, 145.0, 0.12, 0.85, 34),
|
||||
bingx=BingxProfile(4.0, 0.30, 110, 5.0, 2.0, 4))
|
||||
|
||||
ATOM = _b("ATOMUSDT", "mid_cap_l1", reference_price=8.0,
|
||||
depth=DepthProfile(25_000, 0.92, 0.22, 45_000, 700_000),
|
||||
spread=SpreadProfile(2.00, 10.0),
|
||||
vol=VolatilityProfile(76.0, 160.0, 0.13, 0.84, 30),
|
||||
correlation=CorrelationProfile(0.78, 0.48, 0.87),
|
||||
bingx=BingxProfile(6.0, 0.20, 130, 5.0, 2.0, 5))
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Query functions
|
||||
# ==============================================================================
|
||||
|
||||
def get_behavior(symbol: str) -> Optional[AssetBehavior]:
|
||||
return ASSET_BEHAVIORS.get(symbol)
|
||||
|
||||
|
||||
def list_behavior_symbols() -> List[str]:
|
||||
return list(ASSET_BEHAVIORS.keys())
|
||||
|
||||
|
||||
def get_behaviors_by_sector(sector: Sector) -> List[AssetBehavior]:
|
||||
from malkhut.training.asset_classification import get_assets_by_sector
|
||||
return [ASSET_BEHAVIORS[p.symbol] for p in get_assets_by_sector(sector)
|
||||
if p.symbol in ASSET_BEHAVIORS]
|
||||
|
||||
|
||||
def get_behaviors_by_role(role: TokenRole) -> List[AssetBehavior]:
|
||||
from malkhut.training.asset_classification import get_assets_by_token_role
|
||||
return [ASSET_BEHAVIORS[p.symbol] for p in get_assets_by_token_role(role)
|
||||
if p.symbol in ASSET_BEHAVIORS]
|
||||
|
||||
|
||||
def get_behaviors_by_template(template_name: str) -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.template_name == template_name]
|
||||
|
||||
|
||||
def get_behaviors_by_volatility_band(min_ann: float = 0.0, max_ann: float = 500.0) -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values()
|
||||
if min_ann <= b.vol.annualized_normal <= max_ann]
|
||||
|
||||
|
||||
def get_fast_cascade_assets() -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.liquidation.speed == "fast"]
|
||||
|
||||
|
||||
def get_institutional_assets() -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.retail.ratio < 0.5]
|
||||
|
||||
|
||||
def get_retail_dominated_assets() -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.retail.ratio >= 0.6]
|
||||
|
||||
|
||||
def get_high_vol_assets() -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.vol.annualized_normal >= 75.0]
|
||||
|
||||
|
||||
def get_thin_book_assets() -> List[AssetBehavior]:
|
||||
return [b for b in ASSET_BEHAVIORS.values() if b.depth.amplitude_usd < 100_000]
|
||||
530
MALKHUT/malkhut/training/asset_classification.py
Normal file
530
MALKHUT/malkhut/training/asset_classification.py
Normal file
@@ -0,0 +1,530 @@
|
||||
"""
|
||||
Asset Classification — INVARIANT characteristics only.
|
||||
|
||||
Split into two axes:
|
||||
FUNDAMENTAL — intrinsic to the token's design (never changes):
|
||||
Sector*, TokenRole*, SupplyModel, Consensus, SmartContractCapability
|
||||
(* multi-label: an asset CAN belong to multiple sectors/roles)
|
||||
|
||||
TECHNICAL — invariant market-structure properties (set at listing, rarely change):
|
||||
MarketCapTier, TypicalSpread/Depth, TickSize/LotSize, FeeStructure,
|
||||
DerivativeAccess, PriceDecimals, TypicalVolume
|
||||
|
||||
Overlap handling (industry standard, per CoinGecko/CMC/Messari):
|
||||
- Sector & TokenRole: frozenset (multi-label). An asset can be LAYER1 + DEFI,
|
||||
or GAS + GOVERNANCE. The FIRST element is "primary" for single-label compat.
|
||||
- Consensus, SupplyModel, MarketCapTier, SmartContractCapability: single enum.
|
||||
One chain = one consensus. One supply mechanism. Inherently singular.
|
||||
|
||||
Sources: CoinGecko/CMC sector taxonomy (multi-tag per coin), Messari framework,
|
||||
BIS/IMF digital asset classification, MiCA regulatory taxonomy,
|
||||
academic factor models (Cong & He 2019, Harvey/Ramachandran/Santoro 2021),
|
||||
exchange listing standards (Binance, BingX).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# FUNDAMENTAL dimensions — intrinsic to the token, never change
|
||||
# ==============================================================================
|
||||
|
||||
class Sector(str, Enum):
|
||||
"""Primary use-case / industry vertical. CoinGecko + Messari + CMC consensus.
|
||||
Multi-label: an asset CAN appear in multiple sectors (e.g. BNB = EXCHANGE + LAYER1)."""
|
||||
CURRENCY = "currency" # BTC: pure peer-to-peer money
|
||||
LAYER1 = "layer1" # ETH, SOL, ADA, AVAX, DOT, ATOM: smart-contract platform
|
||||
LAYER2 = "layer2" # MATIC, ARB, OP: scaling solutions
|
||||
DEFI = "defi" # UNI, AAVE, MKR: decentralized finance protocols
|
||||
ORACLE = "oracle" # LINK: external data feeds
|
||||
EXCHANGE = "exchange" # BNB, OKB: CEX utility tokens
|
||||
MEME = "meme" # DOGE, SHIB: community/speculation driven
|
||||
PRIVACY = "privacy" # XMR, ZEC: privacy-preserving chains
|
||||
STORAGE = "storage" # FIL, AR: decentralized storage
|
||||
GAMING_NFT = "gaming_nft" # AXS, SAND, IMX: gaming/metaverse
|
||||
|
||||
|
||||
class TokenRole(str, Enum):
|
||||
"""Functional role of the token within its ecosystem. Determines demand elasticity.
|
||||
Multi-label: an asset CAN serve multiple roles (e.g. ETH = GAS + STORE_OF_VALUE + GOVERNANCE)."""
|
||||
GAS = "gas" # Must hold to pay tx fees (ETH, SOL, ADA, AVAX, DOT, ATOM, BNB)
|
||||
STORE_OF_VALUE = "store_of_value" # Digital gold narrative (BTC, ETH)
|
||||
GOVERNANCE = "governance" # Voting rights (UNI, AAVE, MKR, DOT)
|
||||
UTILITY = "utility" # Pays for a service (LINK data, FIL storage)
|
||||
MEME = "meme" # No utility, pure speculation (DOGE)
|
||||
EXCHANGE_FEE = "exchange_fee" # Fee discount / burn (BNB)
|
||||
|
||||
|
||||
class SupplyModel(str, Enum):
|
||||
"""How new tokens enter circulation. Determines long-term supply pressure.
|
||||
Single-label: one asset = one supply mechanism."""
|
||||
FIXED_CAP = "fixed_cap" # Hard cap: BTC (21M), AAVE (16M)
|
||||
DISINFLATIONARY = "disinflationary" # Issuance decreases over time (ETH post-merge)
|
||||
INFLATIONARY = "inflationary" # Ongoing issuance (SOL ~5%, ADA ~3%, AVAX ~4%)
|
||||
BURN_MECHANISM = "burn_mechanism" # Buyback-and-burn (BNB quarterly burns)
|
||||
|
||||
|
||||
class ConsensusFamily(str, Enum):
|
||||
"""Consensus mechanism. Determines miner/validator selling behaviour.
|
||||
Single-label: one chain = one consensus."""
|
||||
POW = "pow" # Must sell to cover electricity → constant sell pressure
|
||||
POS = "pos" # Can hold, staking yield → lower forced selling
|
||||
DPOS = "dpos" # Delegated PoS (EOS, TRX, BNB Chain) → concentrated validator set
|
||||
|
||||
|
||||
class SmartContractCapability(str, Enum):
|
||||
"""Ability to run arbitrary smart contracts. Determines DeFi composability.
|
||||
Single-label: one chain = one capability level."""
|
||||
FULL = "full" # EVM or equivalent (ETH, SOL, ADA, AVAX, DOT, ATOM, UNI, AAVE)
|
||||
PARTIAL = "partial" # Limited scripting (BNB Chain — EVM-compatible but different governance)
|
||||
NONE = "none" # No smart contracts (BTC, DOGE)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# TECHNICAL dimensions — invariant market-structure properties
|
||||
# ==============================================================================
|
||||
|
||||
class MarketCapTier(str, Enum):
|
||||
"""Absolute market-cap band. Changes slowly → semi-invariant. Determines price-impact per dollar."""
|
||||
MEGA = "mega" # >$500B (BTC)
|
||||
LARGE = "large" # $50-500B (ETH, BNB)
|
||||
MID = "mid" # $5-50B (SOL, ADA, AVAX, DOT, ATOM)
|
||||
SMALL = "small" # $500M-5B (UNI, AAVE, LINK, MATIC)
|
||||
MICRO = "micro" # <$500M
|
||||
|
||||
|
||||
class DerivativeAccess(str, Enum):
|
||||
"""Derivatives availability. Affects shorting, funding dynamics, price discovery.
|
||||
Single-label: one asset = one access level on a given exchange."""
|
||||
PERPS_AND_OPTIONS = "perps_and_options" # BTC, ETH
|
||||
PERPS_ONLY = "perps_only" # Most mid/small caps
|
||||
NONE = "none" # Cannot be shorted on CEX
|
||||
|
||||
|
||||
class VolatilityProfile(str, Enum):
|
||||
"""Long-run average volatility band. Semi-invariant statistical fingerprint."""
|
||||
LOW = "low" # <30% annualized (BTC, stablecoins)
|
||||
MEDIUM = "medium" # 30-80% (ETH, ADA)
|
||||
HIGH = "high" # 80-150% (SOL, DOGE, AVAX)
|
||||
EXTREME = "extreme" # >150% (micro-caps, new listings)
|
||||
|
||||
|
||||
class LiquidityProfile(str, Enum):
|
||||
"""Long-run average liquidity tier. Semi-invariant order-book fingerprint."""
|
||||
DEEP = "deep" # >$100M daily (BTC, ETH)
|
||||
NORMAL = "normal" # $10-100M (SOL, BNB, DOGE)
|
||||
THIN = "thin" # $1-10M (UNI, AAVE, MATIC)
|
||||
ILLIQUID = "illiquid" # <$1M
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# AssetProfile — frozen, all-invariant, multi-label where appropriate
|
||||
# ==============================================================================
|
||||
|
||||
@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
|
||||
|
||||
# --- Fundamental (intrinsic, never changes) ---
|
||||
sectors: tuple[Sector, ...] # multi-label: BNB = (EXCHANGE, LAYER1). First = primary.
|
||||
token_roles: tuple[TokenRole, ...] # multi-label: ETH = (GAS, STORE_OF_VALUE, GOVERNANCE). First = primary.
|
||||
supply_model: SupplyModel # single-label
|
||||
consensus: ConsensusFamily # single-label
|
||||
smart_contracts: SmartContractCapability # single-label
|
||||
|
||||
# --- Technical (invariant market-structure properties) ---
|
||||
market_cap_tier: MarketCapTier
|
||||
volatility_profile: VolatilityProfile
|
||||
liquidity_profile: LiquidityProfile
|
||||
derivative_access: DerivativeAccess
|
||||
|
||||
# --- Execution parameters (exchange-set, invariant) ---
|
||||
tick_size: float
|
||||
lot_size: float
|
||||
price_decimals: int
|
||||
maker_fee_bps: float
|
||||
taker_fee_bps: float
|
||||
|
||||
# --- Order-book fingerprint (long-run averages, semi-invariant) ---
|
||||
typical_spread_bps: float
|
||||
typical_depth_usd: float
|
||||
typical_daily_volume_usd: float
|
||||
|
||||
# --- Structural flags (invariant) ---
|
||||
has_funding: bool = False
|
||||
has_options: bool = False
|
||||
|
||||
@property
|
||||
def sector(self) -> Sector:
|
||||
"""Primary sector (first in tuple). For single-label consumers."""
|
||||
return self.sectors[0]
|
||||
|
||||
@property
|
||||
def token_role(self) -> TokenRole:
|
||||
"""Primary role (first in tuple). For single-label consumers."""
|
||||
return self.token_roles[0]
|
||||
|
||||
@property
|
||||
def is_gas_token(self) -> bool:
|
||||
return TokenRole.GAS in self.token_roles
|
||||
|
||||
@property
|
||||
def is_pure_currency(self) -> bool:
|
||||
return Sector.CURRENCY in self.sectors
|
||||
|
||||
@property
|
||||
def can_be_shorted(self) -> bool:
|
||||
return self.derivative_access != DerivativeAccess.NONE
|
||||
|
||||
@property
|
||||
def supply_pressure(self) -> str:
|
||||
"""Predictive label: 'forced' for PoW miners who must sell,
|
||||
'optional' for PoS validators who can hold."""
|
||||
if self.consensus == ConsensusFamily.POW:
|
||||
return "forced"
|
||||
return "optional"
|
||||
|
||||
@property
|
||||
def demand_elasticity(self) -> str:
|
||||
"""Predictive label: 'inelastic' for gas/store-of-value (must hold),
|
||||
'elastic' for governance/meme/exchange (can choose not to buy)."""
|
||||
if any(r in self.token_roles for r in (TokenRole.GAS, TokenRole.STORE_OF_VALUE)):
|
||||
return "inelastic"
|
||||
return "elastic"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Helper to build multi-label profiles concisely
|
||||
# ==============================================================================
|
||||
|
||||
def _profile(
|
||||
symbol: str,
|
||||
sectors: Sequence[Sector],
|
||||
token_roles: Sequence[TokenRole],
|
||||
supply_model: SupplyModel,
|
||||
consensus: ConsensusFamily,
|
||||
smart_contracts: SmartContractCapability,
|
||||
market_cap_tier: MarketCapTier,
|
||||
volatility_profile: VolatilityProfile,
|
||||
liquidity_profile: LiquidityProfile,
|
||||
derivative_access: DerivativeAccess,
|
||||
tick_size: float,
|
||||
lot_size: float,
|
||||
price_decimals: int,
|
||||
maker_fee_bps: float,
|
||||
taker_fee_bps: float,
|
||||
typical_spread_bps: float,
|
||||
typical_depth_usd: float,
|
||||
typical_daily_volume_usd: float,
|
||||
has_funding: bool = False,
|
||||
has_options: bool = False,
|
||||
) -> AssetProfile:
|
||||
return AssetProfile(
|
||||
symbol=symbol,
|
||||
sectors=tuple(sectors),
|
||||
token_roles=tuple(token_roles),
|
||||
supply_model=supply_model,
|
||||
consensus=consensus,
|
||||
smart_contracts=smart_contracts,
|
||||
market_cap_tier=market_cap_tier,
|
||||
volatility_profile=volatility_profile,
|
||||
liquidity_profile=liquidity_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_daily_volume_usd=typical_daily_volume_usd,
|
||||
has_funding=has_funding, has_options=has_options,
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Profiles — 13 major crypto assets with BingX perps
|
||||
# ==============================================================================
|
||||
|
||||
ASSET_PROFILES: Dict[str, AssetProfile] = {
|
||||
"BTCUSDT": _profile(
|
||||
symbol="BTCUSDT",
|
||||
sectors=[Sector.CURRENCY],
|
||||
token_roles=[TokenRole.STORE_OF_VALUE],
|
||||
supply_model=SupplyModel.FIXED_CAP, consensus=ConsensusFamily.POW,
|
||||
smart_contracts=SmartContractCapability.NONE,
|
||||
market_cap_tier=MarketCapTier.MEGA,
|
||||
volatility_profile=VolatilityProfile.LOW,
|
||||
liquidity_profile=LiquidityProfile.DEEP,
|
||||
derivative_access=DerivativeAccess.PERPS_AND_OPTIONS,
|
||||
tick_size=0.1, lot_size=0.001, price_decimals=1,
|
||||
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,
|
||||
has_funding=True, has_options=True,
|
||||
),
|
||||
"ETHUSDT": _profile(
|
||||
symbol="ETHUSDT",
|
||||
sectors=[Sector.LAYER1, Sector.DEFI],
|
||||
token_roles=[TokenRole.GAS, TokenRole.STORE_OF_VALUE, TokenRole.GOVERNANCE],
|
||||
supply_model=SupplyModel.DISINFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.LARGE,
|
||||
volatility_profile=VolatilityProfile.MEDIUM,
|
||||
liquidity_profile=LiquidityProfile.DEEP,
|
||||
derivative_access=DerivativeAccess.PERPS_AND_OPTIONS,
|
||||
tick_size=0.01, lot_size=0.001, price_decimals=2,
|
||||
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,
|
||||
has_funding=True, has_options=True,
|
||||
),
|
||||
"SOLUSDT": _profile(
|
||||
symbol="SOLUSDT",
|
||||
sectors=[Sector.LAYER1],
|
||||
token_roles=[TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.NORMAL,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"DOGEUSDT": _profile(
|
||||
symbol="DOGEUSDT",
|
||||
sectors=[Sector.MEME, Sector.CURRENCY],
|
||||
token_roles=[TokenRole.MEME, TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POW,
|
||||
smart_contracts=SmartContractCapability.NONE,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.NORMAL,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.00001, lot_size=1.0, price_decimals=5,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"ADAUSDT": _profile(
|
||||
symbol="ADAUSDT",
|
||||
sectors=[Sector.LAYER1],
|
||||
token_roles=[TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.DPOS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.MEDIUM,
|
||||
liquidity_profile=LiquidityProfile.NORMAL,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.0001, lot_size=1.0, price_decimals=4,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"AVAXUSDT": _profile(
|
||||
symbol="AVAXUSDT",
|
||||
sectors=[Sector.LAYER1, Sector.DEFI],
|
||||
token_roles=[TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.NORMAL,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"UNIUSDT": _profile(
|
||||
symbol="UNIUSDT",
|
||||
sectors=[Sector.DEFI],
|
||||
token_roles=[TokenRole.GOVERNANCE, TokenRole.UTILITY],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.SMALL,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.THIN,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"LINKUSDT": _profile(
|
||||
symbol="LINKUSDT",
|
||||
sectors=[Sector.ORACLE],
|
||||
token_roles=[TokenRole.UTILITY],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.SMALL,
|
||||
volatility_profile=VolatilityProfile.MEDIUM,
|
||||
liquidity_profile=LiquidityProfile.THIN,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"BNBUSDT": _profile(
|
||||
symbol="BNBUSDT",
|
||||
sectors=[Sector.EXCHANGE, Sector.LAYER1],
|
||||
token_roles=[TokenRole.EXCHANGE_FEE, TokenRole.GAS],
|
||||
supply_model=SupplyModel.BURN_MECHANISM, consensus=ConsensusFamily.DPOS,
|
||||
smart_contracts=SmartContractCapability.PARTIAL,
|
||||
market_cap_tier=MarketCapTier.LARGE,
|
||||
volatility_profile=VolatilityProfile.MEDIUM,
|
||||
liquidity_profile=LiquidityProfile.DEEP,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.001, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"MATICUSDT": _profile(
|
||||
symbol="MATICUSDT",
|
||||
sectors=[Sector.LAYER2],
|
||||
token_roles=[TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.SMALL,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.THIN,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.0001, lot_size=1.0, price_decimals=4,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"AAVEUSDT": _profile(
|
||||
symbol="AAVEUSDT",
|
||||
sectors=[Sector.DEFI],
|
||||
token_roles=[TokenRole.GOVERNANCE],
|
||||
supply_model=SupplyModel.FIXED_CAP, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.SMALL,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.THIN,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"DOTUSDT": _profile(
|
||||
symbol="DOTUSDT",
|
||||
sectors=[Sector.LAYER1],
|
||||
token_roles=[TokenRole.GAS, TokenRole.GOVERNANCE],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.DPOS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.MEDIUM,
|
||||
liquidity_profile=LiquidityProfile.NORMAL,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.001, lot_size=0.1, price_decimals=3,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
"ATOMUSDT": _profile(
|
||||
symbol="ATOMUSDT",
|
||||
sectors=[Sector.LAYER1],
|
||||
token_roles=[TokenRole.GAS],
|
||||
supply_model=SupplyModel.INFLATIONARY, consensus=ConsensusFamily.POS,
|
||||
smart_contracts=SmartContractCapability.FULL,
|
||||
market_cap_tier=MarketCapTier.MID,
|
||||
volatility_profile=VolatilityProfile.HIGH,
|
||||
liquidity_profile=LiquidityProfile.THIN,
|
||||
derivative_access=DerivativeAccess.PERPS_ONLY,
|
||||
tick_size=0.01, lot_size=0.01, price_decimals=2,
|
||||
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,
|
||||
has_funding=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Query functions — filter by any dimension (multi-label aware)
|
||||
# ==============================================================================
|
||||
|
||||
def get_asset_profile(symbol: str) -> Optional[AssetProfile]:
|
||||
return ASSET_PROFILES.get(symbol)
|
||||
|
||||
|
||||
def list_assets() -> List[str]:
|
||||
return list(ASSET_PROFILES.keys())
|
||||
|
||||
|
||||
def get_assets_by_sector(sector: Sector) -> List[AssetProfile]:
|
||||
"""Match assets where the queried sector is ANY of their sectors."""
|
||||
return [p for p in ASSET_PROFILES.values() if sector in p.sectors]
|
||||
|
||||
|
||||
def get_assets_by_token_role(role: TokenRole) -> List[AssetProfile]:
|
||||
"""Match assets where the queried role is ANY of their roles."""
|
||||
return [p for p in ASSET_PROFILES.values() if role in p.token_roles]
|
||||
|
||||
|
||||
def get_assets_by_supply(model: SupplyModel) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.supply_model == model]
|
||||
|
||||
|
||||
def get_assets_by_consensus(family: ConsensusFamily) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.consensus == family]
|
||||
|
||||
|
||||
def get_assets_by_market_cap(tier: MarketCapTier) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.market_cap_tier == tier]
|
||||
|
||||
|
||||
def get_assets_by_volatility(vol: VolatilityProfile) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.volatility_profile == vol]
|
||||
|
||||
|
||||
def get_assets_by_liquidity(liq: LiquidityProfile) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.liquidity_profile == liq]
|
||||
|
||||
|
||||
def get_assets_by_derivatives(access: DerivativeAccess) -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.derivative_access == access]
|
||||
|
||||
|
||||
def get_gas_tokens() -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if TokenRole.GAS in p.token_roles]
|
||||
|
||||
|
||||
def get_pov_assets() -> List[AssetProfile]:
|
||||
"""Assets with forced sell pressure (PoW miners)."""
|
||||
return [p for p in ASSET_PROFILES.values() if p.supply_pressure == "forced"]
|
||||
|
||||
|
||||
def get_shortable_assets() -> List[AssetProfile]:
|
||||
return [p for p in ASSET_PROFILES.values() if p.can_be_shorted]
|
||||
|
||||
|
||||
def get_multi_sector_assets() -> List[AssetProfile]:
|
||||
"""Assets belonging to more than one sector."""
|
||||
return [p for p in ASSET_PROFILES.values() if len(p.sectors) > 1]
|
||||
|
||||
|
||||
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]
|
||||
520
MALKHUT/malkhut/training/asset_compiler.py
Normal file
520
MALKHUT/malkhut/training/asset_compiler.py
Normal file
@@ -0,0 +1,520 @@
|
||||
"""
|
||||
Asset Compiler — auto-fetches from Binance/BingX APIs and produces
|
||||
system-ready AssetProfile + AssetBehavior for any tradeable symbol.
|
||||
|
||||
Rate-limited (1 req/sec), cached, resumable.
|
||||
|
||||
Usage:
|
||||
compiler = AssetCompiler()
|
||||
result = compiler.compile("XRPUSDT")
|
||||
# result.asset_profile → ready for ScenarioFactory
|
||||
# result.asset_behavior → ready for behavior-driven scenarios
|
||||
|
||||
# Batch compile
|
||||
results = compiler.compile_batch(["XRPUSDT", "HBARUSDT", "APTUSDT"])
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from malkhut.training.asset_classification import (
|
||||
AssetProfile, ASSET_PROFILES,
|
||||
Sector, TokenRole, SupplyModel, ConsensusFamily, SmartContractCapability,
|
||||
MarketCapTier, DerivativeAccess, VolatilityProfile, LiquidityProfile,
|
||||
)
|
||||
from malkhut.training.asset_behavior import (
|
||||
AssetBehavior, BehaviorTemplate, TEMPLATES, ASSET_BEHAVIORS,
|
||||
DepthProfile, SpreadProfile, FlowProfile, VolatilityProfile as BehVol,
|
||||
IntradayProfile, WeekendProfile, CorrelationProfile, MarketMakerProfile,
|
||||
LiquidationProfile, FundingProfile, RetailProfile, BingxProfile,
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Binance API client — rate-limited, cached
|
||||
# ==============================================================================
|
||||
|
||||
class BinanceClient:
|
||||
"""Rate-limited Binance REST API client with response caching."""
|
||||
|
||||
BASE = "https://api.binance.com"
|
||||
FAPI = "https://fapi.binance.com"
|
||||
MIN_INTERVAL_S = 1.0 # 1 req/sec = well under Binance 1200/min limit
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._last_request_s: float = 0.0
|
||||
self._cache: Dict[str, dict] = {}
|
||||
|
||||
def _throttle(self) -> None:
|
||||
elapsed = time.time() - self._last_request_s
|
||||
if elapsed < self.MIN_INTERVAL_S:
|
||||
time.sleep(self.MIN_INTERVAL_S - elapsed)
|
||||
self._last_request_s = time.time()
|
||||
|
||||
def _get(self, url: str) -> dict:
|
||||
if url in self._cache:
|
||||
return self._cache[url]
|
||||
self._throttle()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "MalkhutCompiler/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
self._cache[url] = data
|
||||
return data
|
||||
except (urllib.error.URLError, json.JSONDecodeError, OSError) as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def ticker_24h(self, symbol: str) -> dict:
|
||||
return self._get(f"{self.BASE}/api/v3/ticker/24hr?symbol={symbol}")
|
||||
|
||||
def depth(self, symbol: str, limit: int = 100) -> dict:
|
||||
return self._get(f"{self.BASE}/api/v3/depth?symbol={symbol}&limit={limit}")
|
||||
|
||||
def klines(self, symbol: str, interval: str = "1h", limit: int = 168) -> list:
|
||||
url = f"{self.BASE}/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}"
|
||||
return self._get(url)
|
||||
|
||||
def exchange_info(self, symbol: str) -> dict:
|
||||
data = self._get(f"{self.BASE}/api/v3/exchangeInfo")
|
||||
if "symbols" in data:
|
||||
for s in data["symbols"]:
|
||||
if s.get("symbol") == symbol:
|
||||
return s
|
||||
return {}
|
||||
|
||||
def funding_rate(self, symbol: str, limit: int = 20) -> list:
|
||||
fapi_symbol = symbol.replace("USDT", "-USDT")
|
||||
return self._get(f"{self.FAPI}/fapi/v1/fundingRate?symbol={fapi_symbol}&limit={limit}")
|
||||
|
||||
def open_interest(self, symbol: str) -> dict:
|
||||
fapi_symbol = symbol.replace("USDT", "-USDT")
|
||||
return self._get(f"{self.FAPI}/fapi/v1/openInterest?symbol={fapi_symbol}")
|
||||
|
||||
def ticker_24h_perp(self, symbol: str) -> dict:
|
||||
fapi_symbol = symbol.replace("USDT", "-USDT")
|
||||
return self._get(f"{self.FAPI}/fapi/v1/ticker/24hr?symbol={fapi_symbol}")
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Statistical computation helpers
|
||||
# ==============================================================================
|
||||
|
||||
def _compute_annualized_vol(klines: list) -> float:
|
||||
"""Compute annualized volatility from hourly klines."""
|
||||
if not klines or len(klines) < 10:
|
||||
return 80.0 # default mid-cap
|
||||
returns = []
|
||||
for i in range(1, len(klines)):
|
||||
o = float(klines[i][1])
|
||||
c = float(klines[i][4])
|
||||
if o > 0:
|
||||
returns.append(math.log(c / o))
|
||||
if len(returns) < 5:
|
||||
return 80.0
|
||||
mean_r = sum(returns) / len(returns)
|
||||
var_r = sum((r - mean_r) ** 2 for r in returns) / (len(returns) - 1)
|
||||
hourly_vol = math.sqrt(var_r)
|
||||
return hourly_vol * math.sqrt(8760) * 100 # annualize (8760 hours/year)
|
||||
|
||||
|
||||
def _compute_spread_bps(depth_data: dict) -> float:
|
||||
"""Compute spread in bps from depth snapshot."""
|
||||
bids = depth_data.get("bids", [])
|
||||
asks = depth_data.get("asks", [])
|
||||
if not bids or not asks:
|
||||
return 5.0
|
||||
best_bid = float(bids[0][0])
|
||||
best_ask = float(asks[0][0])
|
||||
mid = (best_bid + best_ask) / 2
|
||||
if mid <= 0:
|
||||
return 5.0
|
||||
return ((best_ask - best_bid) / mid) * 10000
|
||||
|
||||
|
||||
def _compute_depth_profile(depth_data: dict, mid_price: float) -> Tuple[float, float]:
|
||||
"""Fit depth amplitude and alpha from depth snapshot.
|
||||
Returns (amplitude_usd, alpha)."""
|
||||
bids = depth_data.get("bids", [])
|
||||
asks = depth_data.get("asks", [])
|
||||
if not bids or not asks or mid_price <= 0:
|
||||
return 50_000.0, 0.9
|
||||
|
||||
cumulative_usd = 0.0
|
||||
for level in bids[:50]:
|
||||
price = float(level[0])
|
||||
qty = float(level[1])
|
||||
dist_bps = abs(price - mid_price) / mid_price * 10000
|
||||
if dist_bps < 1:
|
||||
cumulative_usd += qty * price
|
||||
|
||||
amplitude = max(cumulative_usd, 1_000)
|
||||
|
||||
bid_depths = []
|
||||
for level in bids[:50]:
|
||||
price = float(level[0])
|
||||
qty = float(level[1])
|
||||
dist_bps = max(abs(price - mid_price) / mid_price * 10000, 0.5)
|
||||
bid_depths.append((dist_bps, qty * price))
|
||||
|
||||
if len(bid_depths) < 5:
|
||||
return amplitude, 0.9
|
||||
|
||||
log_dists = [math.log(d) for d, _ in bid_depths if d > 0]
|
||||
log_depths = [math.log(max(v, 1)) for d, v in bid_depths if d > 0]
|
||||
if len(log_dists) < 5:
|
||||
return amplitude, 0.9
|
||||
|
||||
n = len(log_dists)
|
||||
sum_x = sum(log_dists)
|
||||
sum_y = sum(log_depths)
|
||||
sum_xy = sum(x * y for x, y in zip(log_dists, log_depths))
|
||||
sum_x2 = sum(x * x for x in log_dists)
|
||||
denom = n * sum_x2 - sum_x * sum_x
|
||||
if abs(denom) < 1e-10:
|
||||
return amplitude, 0.9
|
||||
slope = (n * sum_xy - sum_x * sum_y) / denom
|
||||
alpha = max(0.5, min(1.5, -slope + 1.0))
|
||||
return amplitude, alpha
|
||||
|
||||
|
||||
def _compute_order_flow_stats(klines: list) -> dict:
|
||||
"""Compute order flow statistics from klines."""
|
||||
if not klines or len(klines) < 10:
|
||||
return {"median_usd": 500, "p99_usd": 100_000, "avg_usd": 2_000}
|
||||
volumes_usd = []
|
||||
for k in klines:
|
||||
vol = float(k[5]) # quote volume
|
||||
trades = float(k[8]) # number of trades
|
||||
if trades > 0:
|
||||
volumes_usd.append(vol / trades)
|
||||
if not volumes_usd:
|
||||
return {"median_usd": 500, "p99_usd": 100_000, "avg_usd": 2_000}
|
||||
volumes_usd.sort()
|
||||
n = len(volumes_usd)
|
||||
median = volumes_usd[n // 2]
|
||||
p99_idx = min(int(n * 0.99), n - 1)
|
||||
avg = sum(volumes_usd) / n
|
||||
return {"median_usd": median, "p99_usd": volumes_usd[p99_idx], "avg_usd": avg}
|
||||
|
||||
|
||||
def _compute_funding_stats(funding_data: list) -> Tuple[float, float, float]:
|
||||
"""Compute funding rate statistics. Returns (mean_bps, std_bps, positive_pct)."""
|
||||
if not funding_data or isinstance(funding_data, dict):
|
||||
return 0.10, 0.20, 60.0
|
||||
rates = []
|
||||
for entry in funding_data:
|
||||
r = float(entry.get("fundingRate", 0))
|
||||
rates.append(r * 10000) # convert to bps
|
||||
if not rates:
|
||||
return 0.10, 0.20, 60.0
|
||||
mean_r = sum(rates) / len(rates)
|
||||
var_r = sum((r - mean_r) ** 2 for r in rates) / max(len(rates) - 1, 1)
|
||||
std_r = math.sqrt(var_r)
|
||||
pos_pct = sum(1 for r in rates if r > 0) / len(rates) * 100
|
||||
return mean_r, std_r, pos_pct
|
||||
|
||||
|
||||
def _classify_market_cap(mcap_usd: float) -> MarketCapTier:
|
||||
if mcap_usd > 500e9:
|
||||
return MarketCapTier.MEGA
|
||||
if mcap_usd > 50e9:
|
||||
return MarketCapTier.LARGE
|
||||
if mcap_usd > 5e9:
|
||||
return MarketCapTier.MID
|
||||
if mcap_usd > 500e6:
|
||||
return MarketCapTier.SMALL
|
||||
return MarketCapTier.MICRO
|
||||
|
||||
|
||||
def _classify_volatility(ann_vol: float) -> VolatilityProfile:
|
||||
if ann_vol < 30:
|
||||
return VolatilityProfile.LOW
|
||||
if ann_vol < 80:
|
||||
return VolatilityProfile.MEDIUM
|
||||
if ann_vol < 150:
|
||||
return VolatilityProfile.HIGH
|
||||
return VolatilityProfile.EXTREME
|
||||
|
||||
|
||||
def _classify_liquidity(vol_usd: float) -> LiquidityProfile:
|
||||
if vol_usd > 100e6:
|
||||
return LiquidityProfile.DEEP
|
||||
if vol_usd > 10e6:
|
||||
return LiquidityProfile.NORMAL
|
||||
if vol_usd > 1e6:
|
||||
return LiquidityProfile.THIN
|
||||
return LiquidityProfile.ILLIQUID
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Heuristic asset classification (for unknown assets)
|
||||
# ==============================================================================
|
||||
|
||||
_KNOWN_CLASSIFICATIONS: Dict[str, dict] = {
|
||||
"BTCUSDT": {"sector": "currency", "role": "store_of_value", "supply": "fixed_cap",
|
||||
"consensus": "pow", "sc": "none", "deriv": "perps_and_options"},
|
||||
"ETHUSDT": {"sector": "layer1", "role": "gas", "supply": "disinflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_and_options"},
|
||||
"SOLUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"DOGEUSDT": {"sector": "meme", "role": "meme", "supply": "inflationary",
|
||||
"consensus": "pow", "sc": "none", "deriv": "perps_only"},
|
||||
"ADAUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "dpos", "sc": "full", "deriv": "perps_only"},
|
||||
"AVAXUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"UNIUSDT": {"sector": "defi", "role": "governance", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"LINKUSDT": {"sector": "oracle", "role": "utility", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"BNBUSDT": {"sector": "exchange", "role": "exchange_fee", "supply": "burn_mechanism",
|
||||
"consensus": "dpos", "sc": "partial", "deriv": "perps_only"},
|
||||
"MATICUSDT": {"sector": "layer2", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"AAVEUSDT": {"sector": "defi", "role": "governance", "supply": "fixed_cap",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"DOTUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "dpos", "sc": "full", "deriv": "perps_only"},
|
||||
"ATOMUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"XRPUSDT": {"sector": "currency", "role": "utility", "supply": "inflationary",
|
||||
"consensus": "bft", "sc": "partial", "deriv": "perps_only"},
|
||||
"TRXUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "dpos", "sc": "full", "deriv": "perps_only"},
|
||||
"LTCUSDT": {"sector": "currency", "role": "store_of_value", "supply": "fixed_cap",
|
||||
"consensus": "pow", "sc": "none", "deriv": "perps_only"},
|
||||
"NEARUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"APTUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "bft", "sc": "full", "deriv": "perps_only"},
|
||||
"OPUSDT": {"sector": "layer2", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"ARBUSDT": {"sector": "layer2", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"SUIUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "bft", "sc": "full", "deriv": "perps_only"},
|
||||
"PEPEUSDT": {"sector": "meme", "role": "meme", "supply": "fixed_cap",
|
||||
"consensus": "pos", "sc": "none", "deriv": "perps_only"},
|
||||
"WIFUSDT": {"sector": "meme", "role": "meme", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "none", "deriv": "perps_only"},
|
||||
"SEIUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "bft", "sc": "full", "deriv": "perps_only"},
|
||||
"INJUSDT": {"sector": "defi", "role": "utility", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"FILUSDT": {"sector": "storage", "role": "utility", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
"HBARUSDT": {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "bft", "sc": "full", "deriv": "perps_only"},
|
||||
"IMXUSDT": {"sector": "gaming_nft", "role": "utility", "supply": "fixed_cap",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"},
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Compile result
|
||||
# ==============================================================================
|
||||
|
||||
@dataclass
|
||||
class CompileResult:
|
||||
symbol: str
|
||||
asset_profile: Optional[AssetProfile]
|
||||
asset_behavior: Optional[AssetBehavior]
|
||||
raw_data: dict
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Asset Compiler
|
||||
# ==============================================================================
|
||||
|
||||
class AssetCompiler:
|
||||
"""Auto-fetch from Binance/BingX, compute params, produce profiles."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.client = BinanceClient()
|
||||
|
||||
def compile(self, symbol: str) -> CompileResult:
|
||||
"""Compile a single asset from live API data."""
|
||||
warnings = []
|
||||
raw = {}
|
||||
|
||||
ticker = self.client.ticker_24h(symbol)
|
||||
if "error" in ticker or "lastPrice" not in ticker:
|
||||
return CompileResult(symbol=symbol, asset_profile=None, asset_behavior=None,
|
||||
raw_data=ticker, warnings=[f"Failed to fetch ticker: {ticker}"])
|
||||
|
||||
price = float(ticker["lastPrice"])
|
||||
vol_usd_24h = float(ticker.get("quoteVolume", 0))
|
||||
raw["ticker"] = {"price": price, "vol_usd_24h": vol_usd_24h}
|
||||
|
||||
depth_data = self.client.depth(symbol, limit=100)
|
||||
spread_bps = _compute_spread_bps(depth_data)
|
||||
depth_amp, depth_alpha = _compute_depth_profile(depth_data, price)
|
||||
raw["depth"] = {"spread_bps": spread_bps, "amplitude": depth_amp, "alpha": depth_alpha}
|
||||
|
||||
klines = self.client.klines(symbol, "1h", 168)
|
||||
ann_vol = _compute_annualized_vol(klines) if klines else 80.0
|
||||
flow = _compute_order_flow_stats(klines)
|
||||
raw["volatility"] = {"annualized": ann_vol}
|
||||
raw["flow"] = flow
|
||||
|
||||
exch_info = self.client.exchange_info(symbol)
|
||||
tick_size = 0.01
|
||||
lot_size = 0.01
|
||||
price_decimals = 2
|
||||
if "filters" in exch_info:
|
||||
for f in exch_info["filters"]:
|
||||
if f["filterType"] == "PRICE_FILTER":
|
||||
tick_size = float(f["tickSize"])
|
||||
price_decimals = max(0, -int(math.log10(tick_size)) if tick_size > 0 else 2)
|
||||
elif f["filterType"] == "LOT_SIZE":
|
||||
lot_size = float(f["stepSize"])
|
||||
raw["exchange"] = {"tick_size": tick_size, "lot_size": lot_size}
|
||||
|
||||
funding_data = self.client.funding_rate(symbol, limit=20)
|
||||
funding_mean, funding_std, funding_pos = _compute_funding_stats(funding_data)
|
||||
raw["funding"] = {"mean_bps": funding_mean, "std_bps": funding_std, "positive_pct": funding_pos}
|
||||
|
||||
oi_data = self.client.open_interest(symbol)
|
||||
oi_value = float(oi_data.get("openInterest", 0)) * price if "openInterest" in oi_data else 0
|
||||
raw["oi"] = {"value_usd": oi_value}
|
||||
|
||||
klass = _KNOWN_CLASSIFICATIONS.get(symbol, {})
|
||||
if not klass:
|
||||
warnings.append(f"No classification for {symbol} — using defaults")
|
||||
klass = {"sector": "layer1", "role": "gas", "supply": "inflationary",
|
||||
"consensus": "pos", "sc": "full", "deriv": "perps_only"}
|
||||
|
||||
sector_map = {"currency": Sector.CURRENCY, "layer1": Sector.LAYER1,
|
||||
"layer2": Sector.LAYER2, "defi": Sector.DEFI, "oracle": Sector.ORACLE,
|
||||
"exchange": Sector.EXCHANGE, "meme": Sector.MEME, "privacy": Sector.PRIVACY,
|
||||
"storage": Sector.STORAGE, "gaming_nft": Sector.GAMING_NFT}
|
||||
role_map = {"gas": TokenRole.GAS, "store_of_value": TokenRole.STORE_OF_VALUE,
|
||||
"governance": TokenRole.GOVERNANCE, "utility": TokenRole.UTILITY,
|
||||
"meme": TokenRole.MEME, "exchange_fee": TokenRole.EXCHANGE_FEE}
|
||||
supply_map = {"fixed_cap": SupplyModel.FIXED_CAP, "disinflationary": SupplyModel.DISINFLATIONARY,
|
||||
"inflationary": SupplyModel.INFLATIONARY, "burn_mechanism": SupplyModel.BURN_MECHANISM}
|
||||
consensus_map = {"pow": ConsensusFamily.POW, "pos": ConsensusFamily.POS,
|
||||
"dpos": ConsensusFamily.DPOS, "bft": ConsensusFamily.POS}
|
||||
sc_map = {"full": SmartContractCapability.FULL, "partial": SmartContractCapability.PARTIAL,
|
||||
"none": SmartContractCapability.NONE}
|
||||
deriv_map = {"perps_and_options": DerivativeAccess.PERPS_AND_OPTIONS,
|
||||
"perps_only": DerivativeAccess.PERPS_ONLY, "none": DerivativeAccess.NONE}
|
||||
|
||||
mcap_usd = vol_usd_24h * 100 # rough estimate from volume
|
||||
if vol_usd_24h > 1e9:
|
||||
mcap_est = vol_usd_24h * 2
|
||||
elif vol_usd_24h > 100e6:
|
||||
mcap_est = vol_usd_24h * 5
|
||||
else:
|
||||
mcap_est = vol_usd_24h * 10
|
||||
|
||||
asset_profile = AssetProfile(
|
||||
symbol=symbol,
|
||||
sectors=(sector_map.get(klass["sector"], Sector.LAYER1),),
|
||||
token_roles=(role_map.get(klass["role"], TokenRole.GAS),),
|
||||
supply_model=supply_map.get(klass["supply"], SupplyModel.INFLATIONARY),
|
||||
consensus=consensus_map.get(klass["consensus"], ConsensusFamily.POS),
|
||||
smart_contracts=sc_map.get(klass["sc"], SmartContractCapability.FULL),
|
||||
market_cap_tier=_classify_market_cap(mcap_est),
|
||||
volatility_profile=_classify_volatility(ann_vol),
|
||||
liquidity_profile=_classify_liquidity(vol_usd_24h),
|
||||
derivative_access=deriv_map.get(klass["deriv"], DerivativeAccess.PERPS_ONLY),
|
||||
tick_size=tick_size, lot_size=lot_size, price_decimals=price_decimals,
|
||||
maker_fee_bps=-0.2, taker_fee_bps=0.5,
|
||||
typical_spread_bps=spread_bps, typical_depth_usd=depth_amp,
|
||||
typical_daily_volume_usd=vol_usd_24h,
|
||||
has_funding=True,
|
||||
has_options=klass["deriv"] == "perps_and_options",
|
||||
)
|
||||
|
||||
template_name = "mid_cap_l1"
|
||||
if klass["sector"] in ("meme",):
|
||||
template_name = "retail_meme"
|
||||
elif klass["sector"] in ("currency",) and klass["consensus"] == "pow":
|
||||
template_name = "institutional_blue_chip"
|
||||
elif mcap_est > 50e9:
|
||||
template_name = "institutional_blue_chip"
|
||||
|
||||
tmpl = TEMPLATES[template_name]
|
||||
bingx_spread = spread_bps * 5.0 # conservative estimate
|
||||
bingx_depth_ratio = 0.30
|
||||
|
||||
asset_behavior = AssetBehavior.from_template(
|
||||
symbol, tmpl, {
|
||||
"depth": DepthProfile(
|
||||
amplitude_usd=depth_amp, alpha=depth_alpha,
|
||||
fragility_factor=tmpl.depth.fragility_factor,
|
||||
depth_at_10bps_usd=depth_amp * (10 ** (1 - depth_alpha)),
|
||||
depth_at_100bps_usd=depth_amp * (100 ** (1 - depth_alpha)),
|
||||
),
|
||||
"spread": SpreadProfile(normal_bps=spread_bps, stress_multiplier=tmpl.spread.stress_multiplier),
|
||||
"flow": FlowProfile(
|
||||
orders_per_sec_normal=tmpl.flow.orders_per_sec_normal,
|
||||
orders_per_sec_stress=tmpl.flow.orders_per_sec_stress,
|
||||
cancel_fill_ratio=tmpl.flow.cancel_fill_ratio,
|
||||
median_order_usd=flow["median_usd"],
|
||||
p99_order_usd=flow["p99_usd"],
|
||||
avg_trade_usd=flow["avg_usd"],
|
||||
),
|
||||
"vol": BehVol(
|
||||
annualized_normal=ann_vol,
|
||||
annualized_crisis=ann_vol * 2.5,
|
||||
garch_alpha=tmpl.vol.garch_alpha,
|
||||
garch_beta=tmpl.vol.garch_beta,
|
||||
half_life_hours=tmpl.vol.half_life_hours,
|
||||
),
|
||||
"funding": FundingProfile(
|
||||
mean_bps_8h=funding_mean, std_bps_8h=funding_std,
|
||||
positive_pct=funding_pos, basis_typical_bps=abs(funding_mean) * 5,
|
||||
),
|
||||
"bingx": BingxProfile(
|
||||
spread_mult=bingx_spread / max(spread_bps, 0.01),
|
||||
depth_ratio=bingx_depth_ratio,
|
||||
latency_ms=100, taker_fee_bps=5.0, maker_fee_bps=2.0,
|
||||
funding_lag_hours=4,
|
||||
),
|
||||
"liquidation": LiquidationProfile(
|
||||
oi_mcap_ratio=oi_value / max(mcap_est, 1),
|
||||
trigger_pct=tmpl.liquidation.trigger_pct,
|
||||
speed=tmpl.liquidation.speed,
|
||||
recovery=tmpl.liquidation.recovery,
|
||||
),
|
||||
},
|
||||
reference_price=price,
|
||||
)
|
||||
|
||||
return CompileResult(
|
||||
symbol=symbol,
|
||||
asset_profile=asset_profile,
|
||||
asset_behavior=asset_behavior,
|
||||
raw_data=raw,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def compile_batch(self, symbols: List[str]) -> List[CompileResult]:
|
||||
"""Compile multiple assets sequentially (rate-limited)."""
|
||||
results = []
|
||||
for sym in symbols:
|
||||
results.append(self.compile(sym))
|
||||
return results
|
||||
|
||||
def register(self, result: CompileResult) -> bool:
|
||||
"""Register compiled results into the global registries."""
|
||||
if not result.asset_profile or not result.asset_behavior:
|
||||
return False
|
||||
ASSET_PROFILES[result.symbol] = result.asset_profile
|
||||
ASSET_BEHAVIORS[result.symbol] = result.asset_behavior
|
||||
return True
|
||||
|
||||
def compile_and_register(self, symbol: str) -> CompileResult:
|
||||
"""Compile and register in one step."""
|
||||
result = self.compile(symbol)
|
||||
self.register(result)
|
||||
return result
|
||||
1334
MALKHUT/malkhut/training/cma_trainer.py
Normal file
1334
MALKHUT/malkhut/training/cma_trainer.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user