Files
fxhnt/tests/unit/test_no_binance_data.py
Jeroen Grusewski 9989220440 feat(unlock): API-driven unlock calendar — weekly DefiLlama refresh (fail-loud)
The token-unlock cliff calendar was a static unlock_calendar.json imported once,
frozen since 2026-07-13 — it went STALE for weeks silently because the ONLY
nightly caller of the DefiLlama refresh was dropped in the Phase-0b venue
consolidation (assets.py flagged this exact follow-up). This restores an
automated feed.

- adapters/data/unlock_calendar_live.py (reborn, CALENDAR-only): fetch_unlock_events
  pulls DefiLlama emissions + CoinGecko id->ticker, keeps only BEARISH cliffs
  (insiders/privateSale, unlockType=cliff) for the tradeable perp universe,
  cliff_day = ts//86400 (same Unix-epoch-day unit as the warehouse panel, so a
  cliff matches the panel day directly). Injectable HTTP for no-network tests.
  The legacy-venue price-snapshot half is NOT recovered (dead; panel now comes
  from the Bybit warehouse).
- application/unlock_calendar_refresh.py: refresh_unlock_calendar upserts into
  unlock_events (idempotent on sym+cliff_day).
- FAIL-LOUD: an empty/failed pull raises UnlockRefreshError -> the weekly Dagster
  asset turns RED, instead of the silent staleness that caused this. write_events
  never wipes existing rows on [], so a failed week keeps the last-known-good
  calendar.
- orchestration: weekly `unlock_calendar_refresh` asset on its OWN job/schedule
  (Mondays 22:00 UTC) — deliberately off the nightly hot path (a ~350-protocol
  pull is heavy; cliffs are months ahead so weekly suffices).
- CLI `fxhnt unlock-calendar-refresh` for manual triggers.

Verified end-to-end against LIVE DefiLlama with the real 851-symbol Bybit
universe: 6760 bearish-cliff events, 2435 future cliffs (incl. today/tomorrow) —
the frozen calendar was missing all recent ones. 6 unit tests (parse/filter,
cliff_day unit, fail-loud, idempotent upsert, no-wipe-on-fail). Binance guard
updated: unlock_calendar_live is back but legacy-venue-free (grep guard enforces
it); 31 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:06:13 +00:00

127 lines
6.0 KiB
Python

"""Guard (Phase 0b Task 7e): Binance is LITERALLY 0% in src/fxhnt — CODE AND COMMENTS. SUBSTRING guard
(mirrors test_no_alpaca.py): `grep -rin binance src/fxhnt` must return 0 hits.
This SUPERSEDES the narrower Task 7b/7c guard that used to live here (which explicitly scoped itself to
CODE, not comments, and intentionally kept historical/documentary Binance mentions). Task 7e closes that
gap: it retires the crypto_pit Binance fetch/panel/ingest/research surface (`crypto_fetch`,
`crypto_pit_panel`, `ts_trend_strategy`, `silver_crypto`, `silver_ingest_helpers`, the `crypto_pit_dir`
setting, the `backtest-funding` CLI, the `--crypto`/`--source crypto` legs of `fetch`/`warehouse-ingest`)
and rewords every remaining Binance-naming comment. No knowledge is lost — the mirage/venue-history
findings are preserved (just not attributed by name) and are banked in memory under
`project_fxhnt_measured_cost_mirage_and_postgres`.
The module/asset/CLI existence checks below are a SECOND, code-level guard against reintroduction —
complementary to the substring grep, not a replacement for it."""
from __future__ import annotations
import importlib
import pathlib
import subprocess
import pytest
def test_no_binance_symbols_in_src() -> None:
root = pathlib.Path(__file__).parents[2] / "src" / "fxhnt"
hits = subprocess.run(["grep", "-rin", "binance", str(root)], capture_output=True, text=True).stdout
assert not hits.strip(), f"residual Binance references:\n{hits}"
_DELETED_MODULES = [
"fxhnt.adapters.data.binance_funding",
"fxhnt.adapters.data.binance_xsfunding_live",
"fxhnt.adapters.exchange.binance_funding_history",
"fxhnt.adapters.exchange.ccxt_live_price",
# NOTE: `unlock_calendar_live` is BACK (feat/unlock-calendar-api) — but ONLY its DefiLlama-emissions
# CALENDAR half, which is legacy-venue-free (verified by the substring grep above). The price-snapshot
# half that DID depend on the retired venue is gone for good. So it is no longer a "deleted module"; the
# grep guard is what enforces no venue reintroduction, not its absence.
"fxhnt.application.funding_dispersion_eval",
"fxhnt.application.bybit_xvenue_funding_eval",
"fxhnt.application.funding_ingest",
"fxhnt.application.spot_ingest",
"fxhnt.application.basis_panel_builder",
"fxhnt.domain.strategies.funding_carry",
# Task 7c — the Binance worst_basis Parquet side-store + the dormant vendored VRP PoC.
"fxhnt.adapters.data.binance_vision_klines",
"fxhnt.adapters.data.binance_spot_history",
"fxhnt.application.worst_basis_ingest",
"fxhnt.adapters.persistence.worst_basis_store",
"fxhnt.vendor.surfer.surfer_poc",
# Task 7e — the crypto_pit Binance fetch/panel/ingest/research surface.
"fxhnt.adapters.data.crypto_fetch",
"fxhnt.adapters.data.crypto_pit_panel",
"fxhnt.application.ts_trend_strategy",
"fxhnt.adapters.warehouse.silver_crypto",
"fxhnt.application.silver_ingest_helpers",
]
# Binance DATA ingest assets (fed only the retired `features` warehouse funding/spot columns).
_DELETED_INGEST_ASSETS = ("crypto_bars", "crypto_funding", "crypto_spot")
# Binance-specific CLIs (ingest / cross-venue-dispersion / manual delta-neutral-order bridge / crypto_pit
# research).
_DELETED_CLIS = {
"crypto-funding-ingest", "crypto-spot-ingest", "build-basis-panel", "xsfunding-orders",
"funding-dispersion-test", "bybit-xvenue-funding-eval",
"ingest-worst-basis", # Task 7c — the Binance worst_basis Parquet side-store CLI.
"backtest-funding", # Task 7e — its only data source was the crypto_pit panel.
}
# CLIs that MUST survive (Bybit/IBKR paths, Bybit-only measured cost, or the futures-only fetch/ingest).
_KEPT_CLIS = {
"bybit-carry-revalidate", "bybit-edges-eval", "cross-venue-carry-eval",
"compare-measured-precompute", "bybit-ingest-worst-basis", "fetch", "warehouse-ingest",
}
@pytest.mark.parametrize("mod", _DELETED_MODULES)
def test_binance_module_gone(mod: str) -> None:
with pytest.raises(ModuleNotFoundError):
importlib.import_module(mod)
def test_binance_ingest_assets_gone() -> None:
from fxhnt.adapters.orchestration import assets
for a in _DELETED_INGEST_ASSETS:
assert not hasattr(assets, a), f"{a} Binance ingest asset still present"
def test_binance_clis_gone() -> None:
from fxhnt.cli import app
names = {c.name or (c.callback.__name__ if c.callback else None) for c in app.registered_commands}
assert not (_DELETED_CLIS & names), f"binance CLIs still registered: {_DELETED_CLIS & names}"
def test_bybit_ibkr_and_futures_clis_survive() -> None:
"""No over-deletion: the decoupled Bybit/IBKR commands + the futures-only fetch/warehouse-ingest legs
are still wired."""
from fxhnt.cli import app
names = {c.name or (c.callback.__name__ if c.callback else None) for c in app.registered_commands}
missing = _KEPT_CLIS - names
assert not missing, f"a KEPT command was wrongly removed: {missing}"
def test_paper_models_have_no_venue_column() -> None:
"""Guard (Phase 0c): the paper book is single-venue at the SCHEMA level — bybit is fxhnt's sole paper book,
so the vestigial `venue` discriminator column has been dropped from every paper_* table (Task 7f had already
hardcoded every `PaperRepo` read/write to bybit; this removes the now-constant column). The invariant is no
longer 'writes use one hardcoded venue value' but 'the models carry NO venue column at all'."""
from fxhnt.adapters.persistence.cockpit_models import (
PaperBookStateRow,
PaperNavRow,
PaperNavSummaryRow,
PaperPositionRow,
PaperShadowPositionRow,
PaperSleeveRetRow,
PaperTradeRow,
)
paper_models = (
PaperPositionRow, PaperTradeRow, PaperNavRow, PaperNavSummaryRow,
PaperBookStateRow, PaperSleeveRetRow, PaperShadowPositionRow,
)
for model in paper_models:
cols = {c.name for c in model.__table__.columns}
assert "venue" not in cols, f"{model.__name__} still has a venue column: {sorted(cols)}"