← Назад
""" 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}