Files
momentum-system/tests/unit/test_volume_ratio.py
T
momentum devandClaude Fable 5 a12dc8a768 test: de-flake volume-ratio test (real-clock dead zone, not plan 007)
compute_ratios bails when UTC now-16min precedes today's 08:00Z, i.e. nightly
~00:00-04:16 ET; the test's hardcoded 2026-07-06 bars made it KeyError at the
date rollover independent of any code change. Freeze the module clock to a
mid-session UTC time so the window is valid deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:36:00 -04:00

97 lines
3.7 KiB
Python

"""Ratio-corrected IEX volume: the free fix for the 2-5% IEX undercount. Pure tests —
a stub client provides both feeds, a stub con captures the upserted rows."""
from research.ingestion.market_data import ingest_live_minute_bars
from research.ingestion.volume_ratio import compute_ratios
def _bars(n, vol, t0="2026-07-06T13:3{i}:00Z", px=5.0):
return [{"t": f"2026-07-06T13:3{i}:00Z", "o": px, "h": px + .05, "l": px - .05,
"c": px, "v": vol, "vw": px} for i in range(n)]
class _Client:
"""Serves distinct volumes per feed (iex thin, sip fat) + per symbol."""
def __init__(self):
self.by_feed = {
"iex": {"GOOD": _bars(5, 100), "DUST": _bars(5, 10)},
"sip": {"GOOD": _bars(5, 2500), "DUST": _bars(5, 200)},
}
def get_minute_bars(self, symbols, start_iso, end_iso=None, feed=None, limit=10_000):
src = self.by_feed.get(feed or "iex", {})
return {s: src.get(s, []) for s in symbols}
class _Con:
"""Captures executemany rows; enough surface for upsert_minute_bars."""
def __init__(self):
self.rows = []
def executemany(self, sql, rows):
self.rows.extend(rows)
def execute(self, *a, **k):
return self
def test_compute_ratios_measures_and_skips_thin(monkeypatch):
# Freeze the clock inside the stub's bar-date window. compute_ratios bails
# (`end <= start`) when UTC now-16min is before today's 08:00Z — true nightly
# from ~00:00-04:16 ET — so with hardcoded 2026-07-06 bars this test flaked at
# the date rollover regardless of the stub data. Pin a mid-session UTC clock.
import research.ingestion.volume_ratio as vr
from datetime import date as _date, datetime as _dt, timezone
class _FrozenDT:
@staticmethod
def now(tz=None):
return _dt(2026, 7, 6, 18, 0, 0, tzinfo=tz or timezone.utc)
class _FrozenDate:
@staticmethod
def today():
return _date(2026, 7, 6)
monkeypatch.setattr(vr, "datetime", _FrozenDT)
monkeypatch.setattr(vr, "date", _FrozenDate)
r = compute_ratios(_Client(), ["GOOD", "DUST", "MISSING"])
assert r["GOOD"] == 25.0 # 12500 sip / 500 iex
assert "DUST" not in r # sip sample below floor -> untrusted
assert "MISSING" not in r
def test_ingest_scales_volume_and_marks_provider():
con = _Con()
ingest_live_minute_bars(con, _Client(), ["GOOD"], volume_scale={"GOOD": 25.0})
flat = [v for row in con.rows for v in row]
assert 2500 in flat and 100 not in flat # 100-share bars scaled to 2500
assert "alpaca_iex_scaled" in flat
def test_ingest_without_ratio_is_unchanged():
con = _Con()
ingest_live_minute_bars(con, _Client(), ["GOOD"]) # no scale dict
flat = [v for row in con.rows for v in row]
assert 100 in flat and 2500 not in flat
assert "alpaca_iex" in flat and "alpaca_iex_scaled" not in flat
class _Settings:
feed = "sip"
class _SipClient(_Client):
settings = _Settings()
def get_minute_bars(self, symbols, start_iso, end_iso=None, feed=None, limit=10_000):
# live SIP feed: already-consolidated volumes
return {s: self.by_feed["sip"].get(s, []) for s in symbols}
def test_sip_feed_refuses_double_scaling():
# once the paid plan makes the live feed SIP, applying the x25 ratios again would
# over-count 25x — the ingest must refuse to scale a non-IEX feed.
con = _Con()
ingest_live_minute_bars(con, _SipClient(), ["GOOD"], volume_scale={"GOOD": 25.0})
flat = [v for row in con.rows for v in row]
assert 2500 in flat and 62500 not in flat # sip volume kept, NOT re-scaled
assert "alpaca_sip" in flat and "alpaca_iex_scaled" not in flat