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>
97 lines
3.7 KiB
Python
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
|