diff --git a/src/fxhnt/adapters/persistence/bybit_spread.py b/src/fxhnt/adapters/persistence/bybit_spread.py new file mode 100644 index 0000000..bb25e27 --- /dev/null +++ b/src/fxhnt/adapters/persistence/bybit_spread.py @@ -0,0 +1,12 @@ +"""Read the current bybit_real_spread snapshot (coin -> spread_bps). Read-only; a point-in-time cross-section +(all rows share one fetched_at), used as the per-coin slippage FLOOR in the honest-reference model.""" +from __future__ import annotations + +from sqlalchemy import create_engine, text + + +def read_spread_snapshot(dsn: str) -> dict[str, float]: + eng = create_engine(dsn) + with eng.connect() as c: + rows = c.execute(text("select coin, spread_bps from bybit_real_spread")).all() + return {coin: float(bps) for coin, bps in rows if bps is not None} diff --git a/tests/integration/test_honest_reference.py b/tests/integration/test_honest_reference.py index b963a96..53ea618 100644 --- a/tests/integration/test_honest_reference.py +++ b/tests/integration/test_honest_reference.py @@ -82,6 +82,18 @@ def test_no_cap_no_impact_reproduces_gross_reconciliation() -> None: assert abs(out[2] - (0.4*-0.03 + -0.6*0.05)) < 1e-9 +def test_impact_scales_and_floors_at_spread() -> None: + wbd = {1: {"C": (1.0, 0.0)}} # flat return -> net == -cost + turnover = {"C": {1: 1_000_000.0}} + # size $10k of $1M ADV, no cap: impact = 1*sqrt(10000/1e6)*1e4 = 1000 bps -> cost = 1*0.10 + r = honest_sleeve_returns(wbd, turnover, spread_bps={}, aum=10_000.0, participation_cap=1.0, impact_k=1.0) + assert abs(r[1] - (-0.10)) < 1e-6 + # spread floor: impact off, wide spread -> cost == spread fraction + r2 = honest_sleeve_returns(wbd, turnover, spread_bps={"C": 50.0}, aum=10_000.0, + participation_cap=1.0, impact_k=0.0) + assert abs(r2[1] - (-50.0/1e4)) < 1e-9 + + def _seed_flat_funding(store: TimescaleFeatureStore, *, days: int = 120) -> None: """FLAT perp price, spot == perp (zero basis), nonzero cross-sectional funding — the xsfunding sleeve's ONLY source of return here is FUNDING. Mirrors the fixture in test_bybit_book_reconciliation.py; xsfunding diff --git a/tests/unit/test_bybit_spread.py b/tests/unit/test_bybit_spread.py new file mode 100644 index 0000000..2c00451 --- /dev/null +++ b/tests/unit/test_bybit_spread.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine, text + +from fxhnt.adapters.persistence.bybit_spread import read_spread_snapshot + + +def test_read_spread_snapshot_returns_coin_to_bps(tmp_path): + dsn = f"sqlite:///{tmp_path}/s.db" + eng = create_engine(dsn) + with eng.begin() as c: + c.execute(text("create table bybit_real_spread (coin text, spread_bps double, fetched_at text)")) + c.execute(text("insert into bybit_real_spread values ('BTCUSDT', 1.0, 'now'), ('PEPEUSDT', 3.5, 'now')")) + out = read_spread_snapshot(dsn) + assert out == {"BTCUSDT": 1.0, "PEPEUSDT": 3.5}