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