""" Tests for DuckDB asset store — schema, sync, queries, persistence. Covers: - Schema creation and table structure - Sync from in-memory profiles → DuckDB - Asset CRUD (upsert, query, get) - Exchange-asset mapping - Behavior profiles - Cross-exchange queries - Blockchain filtering - Case-insensitive exchange lookup - Persistence across connections - Performance baseline """ import pytest import os from pathlib import Path from malkhut.storage.asset_store import AssetStore @pytest.fixture def store(tmp_path): """Create a fresh DuckDB store for each test.""" db_path = str(tmp_path / "test_assets.duckdb") s = AssetStore(db_path=db_path) yield s s.close() class TestSchema: def test_tables_created(self, store): tables = store.conn.execute( "SELECT table_name FROM information_schema.tables WHERE table_schema='main'" ).fetchall() names = {t[0] for t in tables} assert "assets" in names assert "exchanges" in names assert "asset_exchanges" in names assert "behavior_profiles" in names def test_asset_table_columns(self, store): cols = store.conn.execute( "SELECT column_name FROM information_schema.columns WHERE table_name='assets'" ).fetchall() col_names = [c[0] for c in cols] assert "symbol" in col_names assert "base_asset" in col_names assert "coingecko_id" in col_names assert "sectors" in col_names assert "tick_size" in col_names def test_indexes_created(self, store): indexes = store.conn.execute( "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='assets'" ).fetchall() idx_names = {i[0] for i in indexes} assert any("blockchain" in n for n in idx_names) assert any("coingecko" in n for n in idx_names) class TestSyncFromProfiles: def test_sync_returns_count(self, store): count = store.sync_from_profiles() assert count >= 13 def test_sync_populates_assets(self, store): store.sync_from_profiles() assert store.asset_count() >= 13 def test_sync_populates_exchanges(self, store): store.sync_from_profiles() assert store.exchange_count() >= 3 def test_sync_populates_asset_exchanges(self, store): store.sync_from_profiles() exs = store.get_asset_exchanges("BTCUSDT") assert len(exs) >= 1 def test_sync_populates_behaviors(self, store): store.sync_from_profiles() row = store.conn.execute( "SELECT * FROM behavior_profiles WHERE symbol = 'BTCUSDT'" ).fetchone() assert row is not None def test_sync_is_idempotent(self, store): store.sync_from_profiles() count1 = store.asset_count() store.sync_from_profiles() count2 = store.asset_count() assert count1 == count2 class TestAssetCRUD: def test_get_asset_hit(self, store): store.sync_from_profiles() btc = store.get_asset("BTCUSDT") assert btc is not None assert btc["base_asset"] == "BTC" assert btc["name"] == "BTC" def test_get_asset_miss(self, store): store.sync_from_profiles() assert store.get_asset("NONEXISTENT") is None def test_get_asset_all_fields(self, store): store.sync_from_profiles() btc = store.get_asset("BTCUSDT") assert btc["coingecko_id"] == "bitcoin" assert btc["cmc_id"] == 1 assert btc["blockchain"] == "bitcoin" assert btc["unified_symbol"] == "BTC/USDT" assert btc["quote_currency"] == "USDT" assert btc["tick_size"] == 0.1 assert btc["lot_size"] == 0.001 def test_query_assets_by_blockchain(self, store): store.sync_from_profiles() eth_assets = store.query_assets(blockchain="ethereum") assert len(eth_assets) >= 5 symbols = {a["symbol"] for a in eth_assets} assert "ETHUSDT" in symbols assert "UNIUSDT" in symbols def test_query_assets_by_coingecko(self, store): store.sync_from_profiles() results = store.query_assets(coingecko_id="bitcoin") assert len(results) == 1 assert results[0]["symbol"] == "BTCUSDT" def test_query_assets_by_sector(self, store): store.sync_from_profiles() defi = store.query_assets(sectors=["defi"]) assert len(defi) >= 2 symbols = {a["symbol"] for a in defi} assert "UNIUSDT" in symbols def test_asset_count(self, store): store.sync_from_profiles() assert store.asset_count() == 13 class TestExchangeMapping: def test_symbols_for_exchange(self, store): store.sync_from_profiles() binance_assets = store.symbols_for_exchange("binance") assert len(binance_assets) >= 13 def test_symbols_for_exchange_case_insensitive(self, store): store.sync_from_profiles() upper = store.symbols_for_exchange("BINANCE") lower = store.symbols_for_exchange("binance") assert upper == lower def test_symbols_for_unknown_exchange(self, store): store.sync_from_profiles() assert store.symbols_for_exchange("KRAKEN") == [] def test_assets_on_blockchain(self, store): store.sync_from_profiles() eth = store.assets_on_blockchain("ethereum") assert len(eth) >= 5 def test_assets_on_unknown_blockchain(self, store): store.sync_from_profiles() assert store.assets_on_blockchain("nonexistent") == [] class TestBehaviorProfiles: def test_behavior_stored(self, store): store.sync_from_profiles() row = store.conn.execute( "SELECT depth_amplitude_usd, vol_annualized_normal FROM behavior_profiles WHERE symbol='BTCUSDT'" ).fetchone() assert row[0] == 750_000.0 assert row[1] == 35.0 def test_behavior_template(self, store): store.sync_from_profiles() row = store.conn.execute( "SELECT template_name FROM behavior_profiles WHERE symbol='BTCUSDT'" ).fetchone() assert row[0] == "institutional_blue_chip" def test_behavior_reference_price(self, store): store.sync_from_profiles() row = store.conn.execute( "SELECT reference_price FROM behavior_profiles WHERE symbol='BTCUSDT'" ).fetchone() assert row[0] == 64000.0 def test_behavior_liquidation(self, store): store.sync_from_profiles() row = store.conn.execute( "SELECT liq_speed, liq_recovery FROM behavior_profiles WHERE symbol='BTCUSDT'" ).fetchone() assert row[0] == "slow" assert row[1] == "fast" class TestPersistence: def test_data_survives_reopen(self, tmp_path): db_path = str(tmp_path / "persist.duckdb") s1 = AssetStore(db_path=db_path) s1.sync_from_profiles() s1.close() s2 = AssetStore(db_path=db_path) assert s2.asset_count() >= 13 btc = s2.get_asset("BTCUSDT") assert btc["base_asset"] == "BTC" s2.close() def test_exchange_mapping_survives(self, tmp_path): db_path = str(tmp_path / "persist.duckdb") s1 = AssetStore(db_path=db_path) s1.sync_from_profiles() exs = s1.get_asset_exchanges("BTCUSDT") s1.close() s2 = AssetStore(db_path=db_path) exs2 = s2.get_asset_exchanges("BTCUSDT") assert exs == exs2 s2.close() class TestPerformanceBaseline: def test_sync_speed(self, store): import time t0 = time.time() store.sync_from_profiles() elapsed = time.time() - t0 assert elapsed < 1.0, f"Sync took {elapsed:.2f}s" def test_query_speed(self, store): import time store.sync_from_profiles() t0 = time.time() for _ in range(100): store.query_assets(blockchain="ethereum") elapsed = time.time() - t0 assert elapsed < 1.0, f"100 queries took {elapsed:.2f}s" def test_get_speed(self, store): import time store.sync_from_profiles() t0 = time.time() for _ in range(1000): store.get_asset("BTCUSDT") elapsed = time.time() - t0 assert elapsed < 1.0, f"1000 gets took {elapsed:.2f}s"