feat(ingestor): enrich market metadata with gamma-api volume/liquidity (#107)

The CLOB API does not expose 24h volume or order-book liquidity, so the
size_anomaly detector currently has no real ratio to compare a trade
against and falls back to the niche-base 0.2 confidence floor. That makes
the volume_impact / book_impact thresholds essentially dead code.

This change adds a small client for the public gamma-api markets endpoint
and merges its volume24hr / liquidityNum snapshot into MarketMetadata
during the existing periodic sync. The detector can now compute real
volume and book impact ratios.

Notes on the gamma client:
- gamma-api enforces a server-side max of 100 markets per page and caps
  `offset` around 10000. The client paginates with bounded concurrency,
  sorted by `volume24hr desc`, so the most-traded markets (which is where
  size anomalies actually matter) are always covered. Markets beyond that
  window have negligible recent volume and the niche path handles them
  fine without a ratio.
- Failures are swallowed: a degraded gamma endpoint must not stop CLOB
  metadata from being cached, since size_anomaly + the niche path remain
  functional with `daily_volume=None`.

MarketMetadata gains three optional Decimal fields (daily_volume,
weekly_volume, liquidity); to_dict / from_dict round-trip is preserved
and older cache entries without these keys deserialize cleanly.

Tests: 12 new tests for GammaClient (parsing, single-page, short-page
stop, offset-cap clean stop, retry, malformed responses); existing
metadata_sync tests updated to inject a mocked GammaClient so they don't
hit the real network.

Co-authored-by: schrodinger01 <schrodinger01@users.noreply.github.com>
This commit is contained in:
Patrick Selamy
2026-06-14 15:17:46 -04:00
committed by GitHub
co-authored by schrodinger01
parent 5afbb35ee9
commit e003e4ba47
5 changed files with 580 additions and 31 deletions
@@ -399,6 +399,12 @@ class MarketMetadata:
# Derived metadata
category: str = "other"
# Liquidity/volume snapshot (from gamma-api). All optional — older
# cache entries and CLOB-only sync results may not have these.
daily_volume: Decimal | None = None
weekly_volume: Decimal | None = None
liquidity: Decimal | None = None
# Cache metadata
last_updated: datetime = field(default_factory=lambda: datetime.now(UTC))
@@ -446,6 +452,9 @@ class MarketMetadata:
"active": self.active,
"closed": self.closed,
"category": self.category,
"daily_volume": str(self.daily_volume) if self.daily_volume is not None else None,
"weekly_volume": str(self.weekly_volume) if self.weekly_volume is not None else None,
"liquidity": str(self.liquidity) if self.liquidity is not None else None,
"last_updated": self.last_updated.isoformat(),
}
@@ -477,6 +486,15 @@ class MarketMetadata:
else:
last_updated = datetime.now(UTC)
def _opt_dec(key: str) -> Decimal | None:
raw = data.get(key)
if raw is None or raw == "":
return None
try:
return Decimal(str(raw))
except (ValueError, ArithmeticError):
return None
return cls(
condition_id=str(data["condition_id"]),
question=str(data.get("question", "")),
@@ -486,5 +504,8 @@ class MarketMetadata:
active=bool(data.get("active", True)),
closed=bool(data.get("closed", False)),
category=str(data.get("category", "other")),
daily_volume=_opt_dec("daily_volume"),
weekly_volume=_opt_dec("weekly_volume"),
liquidity=_opt_dec("liquidity"),
last_updated=last_updated,
)