78 lines
3.8 KiB
Python
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)
|