From 7f1ebe9cab0e5624787b84b8c885423c31e73729 Mon Sep 17 00:00:00 2001 From: momentum dev Date: Fri, 25 Sep 2026 11:14:11 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20read=20the=20data=20feed=20through=20the?= =?UTF-8?q?=20Tradier=20wrapper=20=E2=80=94=20live=20volume=20was=2080-160?= =?UTF-8?q?x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With BROKER=tradier the loop's `client` is a TradierClient that delegates market data to the Alpaca client in `data_client`. Two places read the feed as getattr(client.settings, "feed", "iex"). TradierSettings has no feed, so both read "iex" even though the container runs ALPACA_DATA_FEED=sip (verified in the running loop and in config_overrides): - step_vratio kept computing IEX->SIP ratios as if on the free feed - ingest_live_minute_bars multiplied the real SIP volume by those ratios Checked against the paper stack's SIP bars for the same minutes: prices are identical and unscaled volume matches 1.00x, but scaled rows run x83 (GLND), x117-126 (GRML) and x163 (SKYQ). The 2026-09-25 GRML signal logged a cum_volume of 1.39 billion shares and an rvol of 64.6. For any name with a ratio, every volume gate — rvol, the 300k day-volume floor, min dollar volume, the 2.5%-of-volume liquidity cap, and the gap x rvol ranking — has been reading inflated numbers since the stack went live on 2026-07-31. data_feed() reads through data_client when present. The regression test wraps a SIP client in a TradierClient and fails without the fix. Co-Authored-By: Claude Opus 5.5 --- research/ingestion/market_data.py | 13 ++++++++++++- run_live_paper.py | 3 ++- tests/unit/test_volume_ratio.py | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/research/ingestion/market_data.py b/research/ingestion/market_data.py index 64e087f..8f06f47 100644 --- a/research/ingestion/market_data.py +++ b/research/ingestion/market_data.py @@ -188,6 +188,17 @@ def upsert_daily_bars(con, symbol: str, bars: list[dict]) -> int: return len(rows) +def data_feed(client) -> str: + """The feed a client's market-data calls actually use. A broker wrapper + (TradierClient) carries its OWN settings and delegates data to the Alpaca + client in ``data_client``, so read the feed through it. Reading + ``client.settings.feed`` on the wrapper fell back to "iex", and on the live + Tradier stack that multiplied real SIP volume by the IEX->SIP ratios + (80-160x over-count, 2026-07-31..09-25).""" + src = getattr(client, "data_client", None) or client + return getattr(getattr(src, "settings", None), "feed", "iex") + + def ingest_live_minute_bars( con, client, @@ -231,7 +242,7 @@ def ingest_live_minute_bars( # feed is SIP (paid plan), volume is already consolidated — applying the ~25-300x # ratios again would massively over-count. Resolve the EFFECTIVE feed and refuse # to scale anything that isn't IEX. - effective_feed = feed or getattr(getattr(client, "settings", None), "feed", "iex") + effective_feed = feed or data_feed(client) if effective_feed != "iex": volume_scale = None for symbol, bars in bars_by_symbol.items(): diff --git a/run_live_paper.py b/run_live_paper.py index 6d2c63e..2dbf5a2 100644 --- a/run_live_paper.py +++ b/run_live_paper.py @@ -44,6 +44,7 @@ from api.main import DashboardState, serve_in_background from research.ingestion.market_data import ( IngestionResult, classify_session, + data_feed, discover_active_symbols, ingest_daily_history, ingest_live_minute_bars, @@ -706,7 +707,7 @@ def main(argv: list[str] | None = None) -> int: (ingest_live_minute_bars refuses too; this just saves the API calls).""" if not client: return "skipped (no keys)" - if getattr(client.settings, "feed", "iex") != "iex": + if data_feed(client) != "iex": return "off (live feed is SIP)" from research.ingestion.volume_ratio import compute_ratios, persist_ratios fresh = compute_ratios(client, list(symbols)) diff --git a/tests/unit/test_volume_ratio.py b/tests/unit/test_volume_ratio.py index 511d666..34a011c 100644 --- a/tests/unit/test_volume_ratio.py +++ b/tests/unit/test_volume_ratio.py @@ -94,3 +94,19 @@ def test_sip_feed_refuses_double_scaling(): 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 + + +def test_tradier_wrapper_reads_the_wrapped_feed(): + # the live Tradier stack wraps the Alpaca data client; the wrapper's own + # settings have no `feed`, which used to read as "iex" and re-scale real SIP + # volume by the IEX ratios (the 80-160x live over-count, 2026-07-31..09-25) + from tradier_broker.client import TradierClient + from tradier_broker.settings import TradierSettings + + wrapped = TradierClient(TradierSettings(access_token="t", account_id="a"), + data_client=_SipClient()) + con = _Con() + ingest_live_minute_bars(con, wrapped, ["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 + assert "alpaca_sip" in flat and "alpaca_iex_scaled" not in flat