Files
sentiment-engine/MALKHUT/malkhut/training/asset_bridge.py

143 lines
4.6 KiB
Python
Raw Normal View History

"""
Asset Directory ↔ Classification Bridge
Connects the operational layer (Fable's AssetDirectory — runtime-mutable,
JSON-backed listing status) with the taxonomic layer (our AssetProfile —
frozen, in-code, invariant classification).
Direction: directory → profiles (one-way).
A record in the directory says "this asset EXISTS and is TRADING on venue X".
The bridge propagates that to AssetProfile.exchanges so other consumers
(classification queries, behavior model, ScenarioFactory) can filter by venue.
Usage:
from malkhut.training.asset_bridge import sync_exchanges_from_directory
# After directory is loaded/populated:
sync_exchanges_from_directory() # updates all matching AssetProfile.exchanges
# Or sync a specific asset:
sync_asset_to_profile("BTCUSDT")
"""
from __future__ import annotations
from typing import List, Optional, Set
from malkhut.assets.directory import (
AssetDirectory,
ListingStatus,
normalize_symbol,
)
from malkhut.training.asset_classification import (
ASSET_PROFILES,
AssetProfile,
get_asset_profile,
)
def _get_trading_exchanges(directory: AssetDirectory, symbol: str) -> List[str]:
"""Get all exchanges where an asset has TRADING status."""
rec = directory.get(symbol)
if rec is None:
return []
return [
ex for ex, listing in rec.exchanges.items()
if listing.status == ListingStatus.TRADING
]
def sync_asset_to_profile(
directory: AssetDirectory,
symbol: str,
) -> bool:
"""Sync a single asset's exchange list from directory to profile.
Returns True if the profile was updated, False if not found.
"""
norm = normalize_symbol(symbol)
profile = get_asset_profile(norm)
if profile is None:
return False
trading_exchanges = _get_trading_exchanges(directory, norm)
if not trading_exchanges:
return False
new_exchanges = tuple(sorted(set(profile.exchanges + tuple(trading_exchanges))))
if new_exchanges != profile.exchanges:
# Frozen dataclass — must reconstruct
idx = list(ASSET_PROFILES.keys()).index(norm)
ASSET_PROFILES[norm] = AssetProfile(
symbol=profile.symbol,
base_asset=profile.base_asset,
name=profile.name,
unified_symbol=profile.unified_symbol,
quote_currency=profile.quote_currency,
coingecko_id=profile.coingecko_id,
cmc_id=profile.cmc_id,
blockchain=profile.blockchain,
contract_address=profile.contract_address,
sectors=profile.sectors,
token_roles=profile.token_roles,
supply_model=profile.supply_model,
consensus=profile.consensus,
smart_contracts=profile.smart_contracts,
market_cap_tier=profile.market_cap_tier,
volatility_profile=profile.volatility_profile,
liquidity_profile=profile.liquidity_profile,
derivative_access=profile.derivative_access,
tick_size=profile.tick_size,
lot_size=profile.lot_size,
price_decimals=profile.price_decimals,
maker_fee_bps=profile.maker_fee_bps,
taker_fee_bps=profile.taker_fee_bps,
typical_spread_bps=profile.typical_spread_bps,
typical_depth_usd=profile.typical_depth_usd,
typical_daily_volume_usd=profile.typical_daily_volume_usd,
has_funding=profile.has_funding,
has_options=profile.has_options,
exchanges=new_exchanges,
)
return True
return False
def sync_exchanges_from_directory(
directory: Optional[AssetDirectory] = None,
) -> int:
"""Sync ALL matching assets' exchange lists from directory to profiles.
Returns the number of profiles that were updated.
"""
if directory is None:
directory = AssetDirectory()
updated = 0
for symbol in list(ASSET_PROFILES.keys()):
if sync_asset_to_profile(directory, symbol):
updated += 1
return updated
def get_universe_stats(directory: Optional[AssetDirectory] = None) -> dict:
"""Summary stats: how many directory assets match our profiles, how many don't."""
if directory is None:
directory = AssetDirectory()
matched = 0
unmatched = 0
for sym in directory.records:
norm = normalize_symbol(sym)
if get_asset_profile(norm) is not None:
matched += 1
else:
unmatched += 1
return {
"directory_assets": len(directory),
"matched_profiles": matched,
"unmatched_assets": unmatched,
"profile_count": len(ASSET_PROFILES),
}