from __future__ import annotations import importlib.util import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) _SCRIPT_PATH = Path(__file__).resolve().parents[2] / "scripts" / "enrich_tier2_features.py" _SPEC = importlib.util.spec_from_file_location("enrich_tier2_features", _SCRIPT_PATH) assert _SPEC and _SPEC.loader _MODULE = importlib.util.module_from_spec(_SPEC) _SPEC.loader.exec_module(_MODULE) def test_extract_short_ratio_history_supports_new_oracle_shape() -> None: payload = { "symbol": "AAPL", "history": [ {"date": "2026-03-05", "short_ratio": 0.31}, {"date": "2026-03-06", "short_ratio": 0.29}, ], } history = _MODULE._extract_short_ratio_history(payload) assert history == payload["history"] def test_fetch_short_ratio_uses_history_short_ratio(monkeypatch) -> None: class _Response: status_code = 200 def json(self) -> dict: return { "symbol": "AAPL", "history": [ {"date": "2026-03-07", "short_ratio": 0.50}, {"date": "2026-03-06", "short_ratio": 0.40}, {"date": "2026-03-05", "short_ratio": 0.30}, {"date": "2026-03-04", "short_ratio": 0.20}, {"date": "2026-03-03", "short_ratio": 0.10}, {"date": "2026-03-02", "short_ratio": 0.00}, ], } _MODULE._short_ratio_db_cache.clear() _MODULE._short_ratio_cache.clear() monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_db", lambda ticker: []) monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_finra_cdn", lambda ticker, event_date: []) monkeypatch.setattr(_MODULE._http, "get", lambda *args, **kwargs: _Response()) value = _MODULE.fetch_short_ratio("AAPL", "2026-03-08") assert value == (0.50 + 0.40 + 0.30 + 0.20 + 0.10) / 5 def test_fetch_short_ratio_prefers_db_history(monkeypatch) -> None: _MODULE._short_ratio_db_cache.clear() _MODULE._short_ratio_cache.clear() monkeypatch.setattr( _MODULE, "_fetch_short_ratio_history_from_db", lambda ticker: [ {"date": "2026-03-07", "short_ratio": 0.50}, {"date": "2026-03-06", "short_ratio": 0.40}, {"date": "2026-03-05", "short_ratio": 0.30}, {"date": "2026-03-04", "short_ratio": 0.20}, {"date": "2026-03-03", "short_ratio": 0.10}, ], ) monkeypatch.setattr( _MODULE._http, "get", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("oracle should not be called")), ) value = _MODULE.fetch_short_ratio("AAPL", "2026-03-08") assert value == (0.50 + 0.40 + 0.30 + 0.20 + 0.10) / 5 def test_required_short_ratio_days_tracks_event_date() -> None: days = _MODULE._required_short_ratio_days("2022-03-02") assert 1400 <= days <= 2000 def test_fetch_short_ratio_oracle_uses_dynamic_days(monkeypatch) -> None: calls: list[int] = [] class _Response: status_code = 200 def json(self) -> dict: return { "symbol": "AAPL", "history": [ {"date": "2026-03-07", "short_ratio": 0.50}, {"date": "2026-03-06", "short_ratio": 0.40}, {"date": "2026-03-05", "short_ratio": 0.30}, {"date": "2026-03-04", "short_ratio": 0.20}, {"date": "2026-03-03", "short_ratio": 0.10}, ], } def _fake_get(*args, **kwargs): calls.append(int(kwargs["params"]["days"])) return _Response() _MODULE._short_ratio_db_cache.clear() _MODULE._short_ratio_cache.clear() monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_db", lambda ticker: []) monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_finra_cdn", lambda ticker, event_date: []) monkeypatch.setattr(_MODULE._http, "get", _fake_get) value = _MODULE.fetch_short_ratio("AAPL", "2026-03-08") assert value == (0.50 + 0.40 + 0.30 + 0.20 + 0.10) / 5 assert calls and calls[0] >= 60 def test_fetch_short_ratio_falls_back_to_finra_daily_files(monkeypatch) -> None: class _Response: status_code = 200 def __init__(self, text: str) -> None: self.text = text payloads = { "20260307": "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n20260307|AAPL|50|0|100|Q\n", "20260306": "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n20260306|AAPL|40|0|100|Q\n", "20260305": "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n20260305|AAPL|30|0|100|Q\n", "20260304": "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n20260304|AAPL|20|0|100|Q\n", "20260303": "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n20260303|AAPL|10|0|100|Q\n", } def _fake_get(url: str, **_kwargs): date_key = url.rsplit("CNMSshvol", 1)[1].split(".txt", 1)[0] return _Response(payloads.get(date_key, "Date|Symbol|ShortVolume|ShortExemptVolume|TotalVolume|Market\n")) _MODULE._short_ratio_db_cache.clear() _MODULE._short_ratio_cache.clear() _MODULE._finra_daily_cache.clear() monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_db", lambda ticker: []) monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_oracle", lambda ticker, days: []) monkeypatch.setattr(_MODULE._http, "get", _fake_get) value = _MODULE.fetch_short_ratio("AAPL", "2026-03-08") assert value == (0.50 + 0.40 + 0.30 + 0.20 + 0.10) / 5 def test_fetch_short_ratio_oracle_timeout_returns_none(monkeypatch) -> None: _MODULE._short_ratio_db_cache.clear() _MODULE._short_ratio_cache.clear() _MODULE._finra_daily_cache.clear() _MODULE._oracle_short_ratio_unavailable = False monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_db", lambda ticker: []) monkeypatch.setattr(_MODULE, "_fetch_short_ratio_history_from_finra_cdn", lambda ticker, event_date: []) def _raise_timeout(*_args, **_kwargs): raise _MODULE.requests.ReadTimeout("oracle unavailable") monkeypatch.setattr(_MODULE._http, "get", _raise_timeout) value = _MODULE.fetch_short_ratio("AAPL", "2026-03-08") assert value is None assert _MODULE._oracle_short_ratio_unavailable is True def test_normalize_sector_name_maps_snapshot_aliases() -> None: assert _MODULE.normalize_sector_name("Consumer Cyclical") == "Consumer Discretionary" assert _MODULE.normalize_sector_name("Financial Services") == "Financials" assert _MODULE.normalize_sector_name("Consumer Defensive") == "Consumer Staples" assert _MODULE.normalize_sector_name("Basic Materials") == "Materials" assert _MODULE.normalize_sector_name("Healthcare") == "Healthcare" def test_fetch_sector_momentum_uses_normalized_sector_alias(monkeypatch) -> None: def _fake_fetch(ticker: str, start: str, end: str) -> list[dict]: dates = [f"2026-01-{day:02d}" for day in range(1, 22)] if ticker == "XLY": return [ {"date": date, "close": 100.0 + idx} for idx, date in enumerate(dates[:-1]) ] + [{"date": dates[-1], "close": 144.0}] if ticker == "SPY": return [ {"date": date, "close": 100.0 + idx} for idx, date in enumerate(dates[:-1]) ] + [{"date": dates[-1], "close": 132.0}] raise AssertionError(f"unexpected ticker {ticker}") monkeypatch.setattr(_MODULE, "_fetch_bars_raw", _fake_fetch) _MODULE._sector_etf_cache.clear() value = _MODULE.fetch_sector_momentum("Consumer Cyclical", "2026-01-21") assert value == 0.12