← Назад"""
Gamma API scanner — finds active weather markets on Polymarket.
Weather markets are multi-outcome events per city/date.
Each event has ~11 temperature bracket markets (e.g., "56-57°F", "≥74°F").
Access via event slug: highest-temperature-in-{city}-on-{month}-{day}-{year}
"""
import re
import json
import time
import requests
from datetime import datetime, timedelta, timezone
from loguru import logger
from config import GAMMA_API_URL
import db
# Cities with known weather markets on Polymarket (~49 active cities)
# Removed 11 dead cities (no markets): sydney, mumbai, berlin, rome, bangkok,
# dubai, osaka, cairo, nairobi, johannesburg, lima
POLYMARKET_CITIES = [
# Verified active (29 original + 20 new = 49)
"nyc", "london", "seoul", "tokyo", "paris", "miami", "toronto",
"hong-kong", "moscow", "houston", "denver", "shanghai", "wuhan",
"ankara", "warsaw", "munich", "amsterdam", "tel-aviv", "madrid",
"los-angeles", "chicago", "seattle", "san-francisco", "singapore",
"beijing", "mexico-city", "buenos-aires", "sao-paulo", "lagos",
# New cities discovered on Polymarket (Apr 2026)
"atlanta", "austin", "busan", "chongqing", "chengdu",
"shenzhen", "guangzhou", "taipei", "jakarta", "manila",
"kuala-lumpur", "helsinki", "istanbul", "milan", "jeddah",
"lucknow", "karachi", "panama-city", "wellington", "cape-town",
]
# Cities that have LOWEST temperature markets (smaller set)
LOWEST_TEMP_CITIES = [
"nyc", "london", "seoul", "tokyo", "miami", "hong-kong",
"shanghai", "paris",
]
# Import CITY_COORDS from config (single source of truth)
from config import CITY_COORDS
MONTH_NAMES = {
1: "january", 2: "february", 3: "march", 4: "april",
5: "may", 6: "june", 7: "july", 8: "august",
9: "september", 10: "october", 11: "november", 12: "december"
}
def scan_weather_markets() -> list[dict]:
"""
Scan Polymarket for active weather events across all cities.
Checks today + next 2 days for each city.
Scans both highest-temperature (all cities) and lowest-temperature (8 cities).
Returns list of parsed events with their bracket markets.
"""
all_events = []
now = datetime.now(timezone.utc)
# Check today, tomorrow, day after
dates = [now + timedelta(days=d) for d in range(3)]
for dt in dates:
date_str = dt.strftime("%Y-%m-%d")
month_name = MONTH_NAMES[dt.month]
day = dt.day
year = dt.year
slug_date = f"{month_name}-{day}-{year}"
# 1) Highest temperature — all cities
for i, city_slug in enumerate(POLYMARKET_CITIES):
slug = f"highest-temperature-in-{city_slug}-on-{slug_date}"
event = fetch_event(slug, city_slug, date_str, metric="high_temp")
if event:
all_events.append(event)
# Throttle to avoid 429 from Gamma API
if i % 5 == 4:
time.sleep(1.0)
# 2) Lowest temperature — subset of cities
for i, city_slug in enumerate(LOWEST_TEMP_CITIES):
slug = f"lowest-temperature-in-{city_slug}-on-{slug_date}"
event = fetch_event(slug, city_slug, date_str, metric="low_temp")
if event:
all_events.append(event)
# Throttle to avoid 429 from Gamma API
if i % 5 == 4:
time.sleep(1.0)
logger.info(f"Scan complete: {len(all_events)} weather events found ({len(POLYMARKET_CITIES)} cities highest + {len(LOWEST_TEMP_CITIES)} cities lowest)")
return all_events
def fetch_event(slug: str, city_slug: str, date: str, metric: str = "high_temp") -> dict | None:
"""Fetch a single weather event by slug"""
try:
resp = requests.get(
f"{GAMMA_API_URL}/events/slug/{slug}",
timeout=10
)
if resp.status_code != 200:
return None
data = resp.json()
raw_markets = data.get("markets", [])
if not raw_markets:
return None
city_data = CITY_COORDS.get(city_slug, {})
# Parse bracket markets
brackets = []
for m in raw_markets:
bracket = parse_bracket_market(m)
if bracket:
# Fill city/date/metric from parent event BEFORE saving
bracket["city"] = city_data.get("name", city_slug)
bracket["date"] = date
bracket["metric"] = metric
brackets.append(bracket)
# Save individual market to DB
db.save_market(bracket)
event = {
"event_id": data.get("id"),
"slug": slug,
"title": data.get("title", ""),
"city_slug": city_slug,
"city_name": city_data.get("name", city_slug),
"date": date,
"lat": city_data.get("lat"),
"lon": city_data.get("lon"),
"tz": city_data.get("tz"),
"brackets": brackets,
"total_markets": len(brackets),
}
logger.debug(f"Found {len(brackets)} brackets for {city_slug} on {date}")
return event
except requests.exceptions.Timeout:
return None
except Exception as e:
logger.error(f"Error fetching {slug}: {e}")
return None
def parse_bracket_market(raw: dict) -> dict | None:
"""Parse a single bracket market from Gamma API event data"""
question = raw.get("question", "")
condition_id = raw.get("conditionId", "")
if not question or not condition_id:
return None
# Extract token IDs (Gamma API returns JSON string, not list)
clob_tokens_raw = raw.get("clobTokenIds", [])
if isinstance(clob_tokens_raw, str):
try:
clob_tokens = json.loads(clob_tokens_raw)
except Exception:
clob_tokens = []
else:
clob_tokens = clob_tokens_raw
yes_token = clob_tokens[0] if len(clob_tokens) > 0 else None
no_token = clob_tokens[1] if len(clob_tokens) > 1 else None
# Extract prices
outcome_prices = raw.get("outcomePrices", "")
yes_price = 0.0
no_price = 0.0
try:
if isinstance(outcome_prices, str) and outcome_prices:
prices = json.loads(outcome_prices)
yes_price = float(prices[0]) if len(prices) > 0 else 0.0
no_price = float(prices[1]) if len(prices) > 1 else 0.0
elif isinstance(outcome_prices, list):
yes_price = float(outcome_prices[0]) if len(outcome_prices) > 0 else 0.0
no_price = float(outcome_prices[1]) if len(outcome_prices) > 1 else 0.0
except Exception:
pass
# Parse temperature bracket from question
bracket = extract_bracket(question)
volume = float(raw.get("volume", 0) or raw.get("volumeNum", 0) or 0)
return {
"id": condition_id,
"question": question,
"city": None, # filled by parent event
"date": None, # filled by parent event
"metric": "high_temp",
"threshold": bracket.get("mid") or bracket.get("value"),
"threshold_low": bracket.get("low"),
"threshold_high": bracket.get("high"),
"threshold_unit": bracket.get("unit", "F"),
"operator": bracket.get("operator", "between"),
"yes_token_id": yes_token,
"no_token_id": no_token,
"yes_price": yes_price,
"no_price": no_price,
"volume": volume,
"end_date": raw.get("endDate"),
"resolution_source": raw.get("resolutionSource", ""),
}
def extract_bracket(question: str) -> dict:
"""
Extract temperature bracket from question text.
Examples:
- "...be 55°F or below..." → {operator: "lte", value: 55, unit: "F"}
- "...be between 56-57°F..." → {operator: "between", low: 56, high: 57, mid: 56.5, unit: "F"}
- "...be 74°F or higher..." → {operator: "gte", value: 74, unit: "F"}
- "...be 13°C or below..." → {operator: "lte", value: 13, unit: "C"}
"""
q = question
# Detect unit
unit = "C" if "°C" in q else "F"
# Pattern: "between X-Y°F/°C"
match = re.search(r'between\s+(\d+)[–-](\d+)\s*°', q)
if match:
low = float(match.group(1))
high = float(match.group(2))
return {"operator": "between", "low": low, "high": high, "mid": (low + high) / 2, "unit": unit}
# Pattern: "be X°F/°C on" (exact single value)
match = re.search(r'be\s+(\d+)\s*°[FC]\s+on', q)
if match:
val = float(match.group(1))
return {"operator": "eq", "value": val, "low": val, "high": val, "mid": val, "unit": unit}
# Pattern: "X°F or below/lower"
match = re.search(r'(\d+)\s*°[FC]\s+or\s+(?:below|lower)', q)
if match:
val = float(match.group(1))
return {"operator": "lte", "value": val, "high": val, "mid": val, "unit": unit}
# Pattern: "X°F or higher/above"
match = re.search(r'(\d+)\s*°[FC]\s+or\s+(?:higher|above|more)', q)
if match:
val = float(match.group(1))
return {"operator": "gte", "value": val, "low": val, "mid": val, "unit": unit}
return {"operator": "unknown", "unit": unit}