โ† Back
โ˜†
"""Weather Bot Configuration"""
import os
from dotenv import load_dotenv

load_dotenv()


def _env_bool(name, default):
    """Parse a boolean env var robustly. A stray value ('True ' with a space,
    '1', 'yes') must not silently flip a safety flag. Falls back to `default`
    only when the var is entirely unset."""
    raw = os.getenv(name)
    if raw is None:
        return default
    return raw.strip().lower() in ("true", "1", "yes", "on")


# === Server ===
PORT = int(os.getenv("PORT", 3201))
HOST = os.getenv("HOST", "0.0.0.0")

# === Polymarket Wallet ===
PRIVATE_KEY = os.getenv("PRIVATE_KEY", "")
PROXY_ADDRESS = os.getenv("PROXY_ADDRESS", "")
SIGNATURE_TYPE = int(os.getenv("SIGNATURE_TYPE", 2))
CHAIN_ID = 137  # Polygon

# === Bot Settings ===
DRY_RUN = _env_bool("DRY_RUN", True)
BET_SIZE = float(os.getenv("BET_SIZE", 1.0))
BANKROLL = float(os.getenv("BANKROLL", 100))
MAX_DAILY_LOSS = float(os.getenv("MAX_DAILY_LOSS", 20))
MAX_OPEN_POSITIONS = int(os.getenv("MAX_OPEN_POSITIONS", 7))   # live-safe default for ~$25 wallet (was 20)
MAX_PER_MARKET = float(os.getenv("MAX_PER_MARKET", 3))         # live-safe default (was 6); .env overrides
MIN_EDGE = float(os.getenv("MIN_EDGE", 0.08))  # 8% โ€” with k=8 smoothing, sweet spot is 8-18% (model max 91.8%)
MAX_EDGE = float(os.getenv("MAX_EDGE", 0.18))  # 18% cap โ€” data: 18%+ edge = 57-70% WR, loses money. Sweet spot 12-18%
BLOCK_YES_SIDE = _env_bool("BLOCK_YES_SIDE", True)  # YES: 42.9% WR, -$13.49 โ€” blocked by default
MIN_BUY_PRICE = float(os.getenv("MIN_BUY_PRICE", 0.72))  # Don't buy below 72ยข โ€” <75ยข = 55-68% WR vs 60-70% breakeven needed
MAX_BUY_PRICE = float(os.getenv("MAX_BUY_PRICE", 0.85))  # Don't buy above 85ยข โ€” R:R too low (82% WR vs 87% breakeven needed)
KELLY_BANKROLL = float(os.getenv("KELLY_BANKROLL", 25))  # Kelly sizing bankroll (BANKROLL=$10k is for risk limits / position count)
MIN_VOLUME = float(os.getenv("MIN_VOLUME", 1000))
COOLDOWN_SAME_MARKET = 3600  # 1 hour
MIN_PRICE = float(os.getenv("MIN_PRICE", 0.05))  # Don't bid below 5ยข โ€” orders never fill
KELLY_FRACTION = float(os.getenv("KELLY_FRACTION", 0.20))  # Fractional Kelly (20% = conservative)
MIN_BET = float(os.getenv("MIN_BET", 1.0))  # Minimum bet size in $
MAX_ENSEMBLE_STD = float(os.getenv("MAX_ENSEMBLE_STD", 7.0))  # Skip if ensemble spread > 7ยฐF (no consensus)
MAX_SLIPPAGE = float(os.getenv("MAX_SLIPPAGE", 0.03))  # Max spread (yes+no deviation from 1.0) โ€” 3ยข

# === Stats Display ===
# Only show trades after this timestamp in UI (old data = uncalibrated model)
STATS_START_DATE = os.getenv("STATS_START_DATE", "2026-05-14 09:00:00")

# === Scan Intervals ===
SCAN_INTERVAL_SEC = 1800    # scan markets every 30 min (1 round/day, fills limit in 1-2 scans)
RESOLVE_CHECK_SEC = 1800    # check resolutions every 30 min (Polymarket resolves with hours delay anyway)
RESOLVE_MIN_HOURS = int(os.getenv("RESOLVE_MIN_HOURS", 18))  # Wait N hours after market date ends before resolving (Open-Meteo archive needs time to finalize)
SUMMARY_HOUR = 23           # daily summary at 23:00
SUMMARY_MINUTE = 59

# === Probability Model ===
# Uncertainty (sigma) in ยฐF by days ahead
# Based on NWS MAE data: same-day ~2.5F, day-1 ~3F, day-2 ~3.5F, day-3 ~4F
# Sigma โ‰ˆ MAE ร— 1.25 to account for tails
SIGMA_BY_DAYS = {
    0: 3.0,   # same day (was 1.5 โ€” too tight, caused overconfidence)
    1: 3.5,   # was 2.0
    2: 4.0,   # was 2.5
    3: 5.0,   # was 4.0
    4: 5.5,   # was 4.0
    5: 6.0,   # was 4.5
}
DEFAULT_SIGMA = 7.0  # 6+ days ahead (was 6.0)

# === Edge Thresholds ===
EDGE_TIERS = [
    {"min_edge": 0.10, "label": "medium",  "bet_multiplier": 2.0},
    {"min_edge": 0.05, "label": "weak",    "bet_multiplier": 1.0},
]

# === Cities โ†’ Coordinates (single source of truth) ===
# Keyed by Polymarket slug. Used by scanner, forecaster, resolver.
CITY_COORDS = {
    "nyc":           {"lat": 40.71, "lon": -74.01, "tz": "America/New_York",      "name": "New York City"},
    "london":        {"lat": 51.51, "lon": -0.13,  "tz": "Europe/London",          "name": "London"},
    "seoul":         {"lat": 37.57, "lon": 126.98, "tz": "Asia/Seoul",             "name": "Seoul"},
    "tokyo":         {"lat": 35.68, "lon": 139.69, "tz": "Asia/Tokyo",             "name": "Tokyo"},
    "paris":         {"lat": 48.86, "lon": 2.35,   "tz": "Europe/Paris",           "name": "Paris"},
    "miami":         {"lat": 25.76, "lon": -80.19, "tz": "America/New_York",       "name": "Miami"},
    "toronto":       {"lat": 43.65, "lon": -79.38, "tz": "America/Toronto",        "name": "Toronto"},
    "hong-kong":     {"lat": 22.32, "lon": 114.17, "tz": "Asia/Hong_Kong",         "name": "Hong Kong"},
    "moscow":        {"lat": 55.76, "lon": 37.62,  "tz": "Europe/Moscow",          "name": "Moscow"},
    "houston":       {"lat": 29.76, "lon": -95.37, "tz": "America/Chicago",        "name": "Houston"},
    "denver":        {"lat": 39.74, "lon": -104.99, "tz": "America/Denver",        "name": "Denver"},
    "shanghai":      {"lat": 31.23, "lon": 121.47, "tz": "Asia/Shanghai",          "name": "Shanghai"},
    "wuhan":         {"lat": 30.59, "lon": 114.31, "tz": "Asia/Shanghai",          "name": "Wuhan"},
    "ankara":        {"lat": 39.93, "lon": 32.86,  "tz": "Europe/Istanbul",        "name": "Ankara"},
    "warsaw":        {"lat": 52.23, "lon": 21.01,  "tz": "Europe/Warsaw",          "name": "Warsaw"},
    "munich":        {"lat": 48.14, "lon": 11.58,  "tz": "Europe/Berlin",          "name": "Munich"},
    "amsterdam":     {"lat": 52.37, "lon": 4.90,   "tz": "Europe/Amsterdam",       "name": "Amsterdam"},
    "tel-aviv":      {"lat": 32.09, "lon": 34.78,  "tz": "Asia/Jerusalem",         "name": "Tel Aviv"},
    "madrid":        {"lat": 40.42, "lon": -3.70,  "tz": "Europe/Madrid",          "name": "Madrid"},
    "los-angeles":   {"lat": 34.05, "lon": -118.24, "tz": "America/Los_Angeles",   "name": "Los Angeles"},
    "chicago":       {"lat": 41.88, "lon": -87.63, "tz": "America/Chicago",        "name": "Chicago"},
    "seattle":       {"lat": 47.61, "lon": -122.33, "tz": "America/Los_Angeles",   "name": "Seattle"},
    "san-francisco": {"lat": 37.77, "lon": -122.42, "tz": "America/Los_Angeles",   "name": "San Francisco"},
    "sydney":        {"lat": -33.87, "lon": 151.21, "tz": "Australia/Sydney",      "name": "Sydney"},
    "mumbai":        {"lat": 19.08, "lon": 72.88,  "tz": "Asia/Kolkata",           "name": "Mumbai"},
    "berlin":        {"lat": 52.52, "lon": 13.41,  "tz": "Europe/Berlin",          "name": "Berlin"},
    "rome":          {"lat": 41.90, "lon": 12.50,  "tz": "Europe/Rome",            "name": "Rome"},
    "bangkok":       {"lat": 13.76, "lon": 100.50, "tz": "Asia/Bangkok",           "name": "Bangkok"},
    "singapore":     {"lat": 1.35,  "lon": 103.82, "tz": "Asia/Singapore",         "name": "Singapore"},
    "dubai":         {"lat": 25.20, "lon": 55.27,  "tz": "Asia/Dubai",             "name": "Dubai"},
    "beijing":       {"lat": 39.90, "lon": 116.41, "tz": "Asia/Shanghai",          "name": "Beijing"},
    "osaka":         {"lat": 34.69, "lon": 135.50, "tz": "Asia/Tokyo",             "name": "Osaka"},
    "mexico-city":   {"lat": 19.43, "lon": -99.13, "tz": "America/Mexico_City",    "name": "Mexico City"},
    "buenos-aires":  {"lat": -34.60, "lon": -58.38, "tz": "America/Argentina/Buenos_Aires", "name": "Buenos Aires"},
    "cairo":         {"lat": 30.04, "lon": 31.24,  "tz": "Africa/Cairo",           "name": "Cairo"},
    "lagos":         {"lat": 6.52,  "lon": 3.38,   "tz": "Africa/Lagos",           "name": "Lagos"},
    "nairobi":       {"lat": -1.29, "lon": 36.82,  "tz": "Africa/Nairobi",         "name": "Nairobi"},
    "johannesburg":  {"lat": -26.20, "lon": 28.05, "tz": "Africa/Johannesburg",    "name": "Johannesburg"},
    "sao-paulo":     {"lat": -23.55, "lon": -46.63, "tz": "America/Sao_Paulo",     "name": "Sรฃo Paulo"},
    "lima":          {"lat": -12.05, "lon": -77.04, "tz": "America/Lima",           "name": "Lima"},
    "atlanta":       {"lat": 33.75, "lon": -84.39, "tz": "America/New_York",       "name": "Atlanta"},
    "austin":        {"lat": 30.27, "lon": -97.74, "tz": "America/Chicago",        "name": "Austin"},
    "busan":         {"lat": 35.18, "lon": 129.08, "tz": "Asia/Seoul",             "name": "Busan"},
    "chongqing":     {"lat": 29.56, "lon": 106.55, "tz": "Asia/Shanghai",          "name": "Chongqing"},
    "chengdu":       {"lat": 30.57, "lon": 104.07, "tz": "Asia/Shanghai",          "name": "Chengdu"},
    "shenzhen":      {"lat": 22.54, "lon": 114.06, "tz": "Asia/Shanghai",          "name": "Shenzhen"},
    "guangzhou":     {"lat": 23.13, "lon": 113.26, "tz": "Asia/Shanghai",          "name": "Guangzhou"},
    "taipei":        {"lat": 25.03, "lon": 121.57, "tz": "Asia/Taipei",            "name": "Taipei"},
    "jakarta":       {"lat": -6.21, "lon": 106.85, "tz": "Asia/Jakarta",           "name": "Jakarta"},
    "manila":        {"lat": 14.60, "lon": 120.98, "tz": "Asia/Manila",            "name": "Manila"},
    "kuala-lumpur":  {"lat": 3.14,  "lon": 101.69, "tz": "Asia/Kuala_Lumpur",      "name": "Kuala Lumpur"},
    "helsinki":      {"lat": 60.17, "lon": 24.94,  "tz": "Europe/Helsinki",        "name": "Helsinki"},
    "istanbul":      {"lat": 41.01, "lon": 28.98,  "tz": "Europe/Istanbul",        "name": "Istanbul"},
    "milan":         {"lat": 45.46, "lon": 9.19,   "tz": "Europe/Rome",            "name": "Milan"},
    "jeddah":        {"lat": 21.49, "lon": 39.19,  "tz": "Asia/Riyadh",            "name": "Jeddah"},
    "lucknow":       {"lat": 26.85, "lon": 80.95,  "tz": "Asia/Kolkata",           "name": "Lucknow"},
    "karachi":       {"lat": 24.86, "lon": 67.01,  "tz": "Asia/Karachi",           "name": "Karachi"},
    "panama-city":   {"lat": 8.98,  "lon": -79.52, "tz": "America/Panama",         "name": "Panama City"},
    "wellington":    {"lat": -41.29, "lon": 174.78, "tz": "Pacific/Auckland",      "name": "Wellington"},
    "cape-town":     {"lat": -33.93, "lon": 18.42, "tz": "Africa/Johannesburg",    "name": "Cape Town"},
}

# Auto-generated lookup by city name (lowercase) for resolver
# Maps "new york city" โ†’ {lat, lon, tz, name}, "hong kong" โ†’ ..., etc.
CITIES_BY_NAME = {data["name"].lower(): data for data in CITY_COORDS.values()}

# === Gamma API ===
GAMMA_API_URL = "https://gamma-api.polymarket.com"

# === Weather APIs ===
# Primary: WeatherAPI.com (1M calls/month free, get key at https://weatherapi.com)
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY", "")
WEATHER_API_URL = "http://api.weatherapi.com/v1/forecast.json"
# Fallback: Open-Meteo (no key, but aggressive rate limits)
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
# Ensemble API: 51-member GFS for probabilistic forecasts (no key needed)
OPEN_METEO_ENSEMBLE_URL = "https://ensemble-api.open-meteo.com/v1/ensemble"
ENSEMBLE_MODEL = "gfs_seamless"  # 31 members (control + member01..member30)
ENSEMBLE_MEMBERS = 31  # temperature_2m (control) + member01..member30
ENSEMBLE_MODEL_ECMWF = "ecmwf_ifs025"  # 51 members (control + member01..member50)

# METAR observations (Aviation Weather Center โ€” free, no key)
METAR_API_URL = "https://aviationweather.gov/api/data/metar"
# Forecast cache TTL โ€” weather doesn't change every 15 min
FORECAST_CACHE_TTL = int(os.getenv("FORECAST_CACHE_TTL", 7200))  # 2 hours

# === CLOB API ===
CLOB_API_URL = "https://clob.polymarket.com"

# === Trade Proxy (bypass geoblock) ===
# Supports HTTP, HTTPS, SOCKS5 โ€” e.g. socks5://user:pass@host:port
TRADE_PROXY = os.getenv("TRADE_PROXY", "")

# === Logging ===
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

๐Ÿ“œ Git History

058de34fix(audit): chunk 4 - minor robustness, display, calibration5 weeks ago
ddaa0a2fix(audit): chunk 2 - kill switch, auth, config safety5 weeks ago
8fca132chore: initial commit โ€” version control setup5 weeks ago
Show last diff
Loading...