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>
127 lines
6.0 KiB
Python
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)}"
|