Files
fxhnt/tests/integration/test_warehouse_table_knob.py

78 lines
3.8 KiB
Python

"""FXHNT_WAREHOUSE_TABLE knob: a single config flag selects WHICH warehouse table the feature store
reads/writes — the default `features` (Binance) vs `bybit_features` (Bybit) — so all edges
(tstrend/unlock/stablecoin/xsfunding) run on Bybit data by flipping the flag, with NO per-sleeve change.
The default is unchanged (`features`); the knob is opt-in via env (`FXHNT_WAREHOUSE_TABLE=bybit_features`).
We stand in for live Timescale with an in-memory sqlite DSN, the same drop-in store the parity tests use.
"""
from __future__ import annotations
from fxhnt.adapters.warehouse import TimescaleFeatureStore, get_feature_store
from fxhnt.config import Settings
def _pg_settings(*, warehouse_table: str | None = None) -> Settings:
kw: dict = {"feature_store": "postgres", "operational_dsn": "sqlite://"}
if warehouse_table is not None:
kw["warehouse_table"] = warehouse_table
return Settings(**kw)
def test_default_setting_is_features() -> None:
"""The default warehouse_table is the Binance `features` table — live behavior preserved."""
assert Settings().warehouse_table == "features"
def test_env_overrides_warehouse_table(monkeypatch) -> None:
"""FXHNT_WAREHOUSE_TABLE drives the setting (env_prefix=FXHNT_)."""
monkeypatch.setenv("FXHNT_WAREHOUSE_TABLE", "bybit_features")
assert Settings().warehouse_table == "bybit_features"
def test_factory_default_table_is_features() -> None:
store = get_feature_store(_pg_settings())
assert isinstance(store, TimescaleFeatureStore)
assert store._table == "features"
def test_factory_routes_to_bybit_table_when_set() -> None:
store = get_feature_store(_pg_settings(warehouse_table="bybit_features"))
assert isinstance(store, TimescaleFeatureStore)
assert store._table == "bybit_features"
def test_book_sim_path_reads_configured_table() -> None:
"""A book/sim read (crypto_funding_panel) honors the knob: with two stores over the SAME engine — one
on `features`, one on `bybit_features` — seeded with DIFFERENT funding, the factory-selected store
returns the data of the configured table, so the resulting panel differs by flag value alone."""
dsn = "sqlite://"
# Two stores over the SAME in-memory engine would each get a private connection; instead, seed each
# table via its own store but on DISTINCT engines, then assert the factory (which builds its own store
# from the DSN) reads the table the setting names. To keep both tables in ONE engine, seed through a
# single store per table sharing the StaticPool by reusing the same DSN object identity is not possible
# for `sqlite://` (each create_engine gets a fresh :memory:). So seed a file-backed sqlite DB instead.
import tempfile, os
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
try:
file_dsn = f"sqlite:///{path}"
binance = TimescaleFeatureStore(file_dsn, table="features")
bybit = TimescaleFeatureStore(file_dsn, table="bybit_features")
# Same symbol + epoch_day, DIFFERENT funding values per table.
ts = 86_400 * 100
binance.write_features("BTCUSDT", [(ts, {"funding": 0.01})])
bybit.write_features("BTCUSDT", [(ts, {"funding": 0.09})])
s_default = Settings(feature_store="postgres", operational_dsn=file_dsn)
s_bybit = Settings(feature_store="postgres", operational_dsn=file_dsn,
warehouse_table="bybit_features")
panel_default = get_feature_store(s_default).crypto_funding_panel()
panel_bybit = get_feature_store(s_bybit).crypto_funding_panel()
assert panel_default["BTCUSDT"][100] == 0.01 # Binance table
assert panel_bybit["BTCUSDT"][100] == 0.09 # Bybit table
assert panel_default != panel_bybit # the knob changed the data the sleeve sees
finally:
os.unlink(path)