malkhut: asset bridge — directory ↔ classification integration
asset_bridge.py: connects Fable's AssetDirectory (operational layer, runtime-mutable, JSON-backed listing status) with our AssetProfile (taxonomic layer, frozen, invariant classification). Functions: - sync_asset_to_profile(directory, symbol): sync one asset's TRADING exchanges from directory to AssetProfile.exchanges - sync_exchanges_from_directory(directory): sync all matching assets - get_universe_stats(directory): matched/unmatched counts 49 tests covering: - normalize_symbol (7 edge cases) - ExchangeListing validation (4 tests) - AssetRecord listing queries (5 tests) - AssetDirectory CRUD + persistence (14 tests) - symbols_for_exchange filtering by status (4 tests) - venue_symbol mapping (3 tests) - Bridge sync (8 tests with state save/restore) - Integration with ScenarioFactory + get_assets_on_exchange (4 tests) Total: 1246 tests across 49 files, all green, zero regressions. Fable's assets/ package preserved intact, interfaces retained.
This commit is contained in:
142
MALKHUT/malkhut/training/asset_bridge.py
Normal file
142
MALKHUT/malkhut/training/asset_bridge.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
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),
|
||||
}
|
||||
Reference in New Issue
Block a user