← back to Zero Code Trading Bot
assets/bot-template/venue_adapter.py
152 lines
"""
venue_adapter.py — normalizes a trading venue into a single interface the bot uses.
adapter = get_adapter("polymarket")
markets = adapter.fetch_universe(query="", limit=200) # list[Market]
price = adapter.mark_price(market_id) # float 0..1
A Market is a plain dict with the variables safe_eval.MARKET_VARS expects, plus
id / title for display. Adapters are READ-ONLY here — they never place orders.
Order routing lives in executor.py so the read path and the money path stay
separate (and the money path stays gated).
Polymarket uses the PUBLIC Gamma API — no key, $0 per call. This is the same
data source the `polymtrx` skill scans, so a bot built here and a /polymtrx scan
see the same market truth.
"""
import json
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone
USER_AGENT = "zero-code-trading-bot/1.0 (paper; +local)"
def _get_json(url, timeout=20):
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def _days_until(iso_ts):
if not iso_ts:
return 9999.0
try:
end = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
return max(0.0, (end - datetime.now(timezone.utc)).total_seconds() / 86400.0)
except (ValueError, TypeError):
return 9999.0
class PolymarketAdapter:
"""Read-only Polymarket via the public Gamma API (gamma-api.polymarket.com)."""
name = "polymarket"
GAMMA = "https://gamma-api.polymarket.com/markets"
def fetch_universe(self, query="", limit=200):
# order=volumeNum (numeric) — plain order=volume sorts lexicographically,
# ranking "$100" above "$154M" and starving the bot of real markets.
params = {"closed": "false", "active": "true", "limit": str(min(limit, 500)),
"order": "volumeNum", "ascending": "false"}
url = self.GAMMA + "?" + urllib.parse.urlencode(params)
raw = _get_json(url)
markets = []
for m in raw:
title = (m.get("question") or m.get("title") or "").strip()
if query and query.lower() not in title.lower():
continue
price = self._yes_price(m)
if price is None:
continue
spread = self._safe_float(m.get("spread"), default=1.0)
markets.append({
"id": str(m.get("id") or m.get("conditionId") or title),
"title": title,
"price": price,
"volume_usd": self._safe_float(m.get("volume") or m.get("volumeNum")),
"liquidity_usd": self._safe_float(m.get("liquidity") or m.get("liquidityNum")),
"spread": spread,
"days_to_resolve": _days_until(m.get("endDate") or m.get("end_date_iso")),
"url": "https://polymarket.com/event/" + str(m.get("slug", "")),
})
return markets
def mark_price(self, market_id):
"""Re-fetch a single market's current YES price (for marking open positions)."""
url = self.GAMMA + "?" + urllib.parse.urlencode({"id": market_id})
try:
raw = _get_json(url)
except Exception:
return None
if not raw:
return None
return self._yes_price(raw[0])
@staticmethod
def _yes_price(m):
prices = m.get("outcomePrices")
if isinstance(prices, str):
try:
prices = json.loads(prices)
except json.JSONDecodeError:
prices = None
if isinstance(prices, list) and prices:
try:
return round(float(prices[0]), 4)
except (ValueError, TypeError):
return None
best = m.get("bestBid")
return round(float(best), 4) if best not in (None, "") else None
@staticmethod
def _safe_float(v, default=0.0):
try:
return float(v)
except (ValueError, TypeError):
return default
class KalshiDemoAdapter:
"""
Kalshi placeholder. Live Kalshi needs real API credentials (KALSHI_API_KEY /
KALSHI_PRIVATE_KEY) — see references/venues.md. Until those are wired and
Steve-approved, this adapter runs in DEMO: it returns a clear stub so a bot
can be authored and paper-tested against Polymarket first, then re-pointed at
Kalshi once credentials exist. (Mirrors the Ken/Kalshi dashboard being DEMO
pending prod creds.)
"""
name = "kalshi"
def fetch_universe(self, query="", limit=200):
raise NotImplementedError(
"Kalshi live data needs API credentials. Author + paper-test the "
"strategy on Polymarket (venue='polymarket'), or wire Kalshi creds "
"per references/venues.md and get Steve's go-live approval first."
)
def mark_price(self, market_id):
return None
_ADAPTERS = {"polymarket": PolymarketAdapter, "kalshi": KalshiDemoAdapter}
def get_adapter(venue):
cls = _ADAPTERS.get((venue or "").lower())
if cls is None:
raise ValueError(f"unknown venue {venue!r}; supported: {sorted(_ADAPTERS)}")
return cls()
if __name__ == "__main__": # quick manual check: python venue_adapter.py
a = get_adapter("polymarket")
t0 = time.time()
ms = a.fetch_universe(limit=50)
print(f"fetched {len(ms)} markets in {time.time()-t0:.1f}s (cost: $0 — public API)")
for m in ms[:5]:
print(f" {m['price']:.2f} vol=${m['volume_usd']:,.0f} {m['title'][:70]}")