fix: read the data feed through the Tradier wrapper — live volume was 80-160x
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2e9687c45
commit
7f1ebe9cab
3 files changed
+30
-2
No files matched your search
@@ -188,6 +188,17 @@ def upsert_daily_bars(con, symbol: str, bars: list[dict]) -> int:
|
|||||||
return len(rows)
|
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(
|
def ingest_live_minute_bars(
|
||||||
con,
|
con,
|
||||||
client,
|
client,
|
||||||
@@ -231,7 +242,7 @@ def ingest_live_minute_bars(
|
|||||||
# feed is SIP (paid plan), volume is already consolidated — applying the ~25-300x
|
# 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
|
# ratios again would massively over-count. Resolve the EFFECTIVE feed and refuse
|
||||||
# to scale anything that isn't IEX.
|
# 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":
|
if effective_feed != "iex":
|
||||||
volume_scale = None
|
volume_scale = None
|
||||||
for symbol, bars in bars_by_symbol.items():
|
for symbol, bars in bars_by_symbol.items():
|
||||||
|
|||||||
+2
-1
@@ -44,6 +44,7 @@ from api.main import DashboardState, serve_in_background
|
|||||||
from research.ingestion.market_data import (
|
from research.ingestion.market_data import (
|
||||||
IngestionResult,
|
IngestionResult,
|
||||||
classify_session,
|
classify_session,
|
||||||
|
data_feed,
|
||||||
discover_active_symbols,
|
discover_active_symbols,
|
||||||
ingest_daily_history,
|
ingest_daily_history,
|
||||||
ingest_live_minute_bars,
|
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)."""
|
(ingest_live_minute_bars refuses too; this just saves the API calls)."""
|
||||||
if not client:
|
if not client:
|
||||||
return "skipped (no keys)"
|
return "skipped (no keys)"
|
||||||
if getattr(client.settings, "feed", "iex") != "iex":
|
if data_feed(client) != "iex":
|
||||||
return "off (live feed is SIP)"
|
return "off (live feed is SIP)"
|
||||||
from research.ingestion.volume_ratio import compute_ratios, persist_ratios
|
from research.ingestion.volume_ratio import compute_ratios, persist_ratios
|
||||||
fresh = compute_ratios(client, list(symbols))
|
fresh = compute_ratios(client, list(symbols))
|
||||||
|
|||||||
@@ -94,3 +94,19 @@ def test_sip_feed_refuses_double_scaling():
|
|||||||
flat = [v for row in con.rows for v in row]
|
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 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
|
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
|
||||||
Reference in new issue
Block a user