From e17fb756f511d6fd0d5981578f176af0cd923536 Mon Sep 17 00:00:00 2001 From: alteregoeth-ai Date: Mon, 9 Mar 2026 02:24:37 -0500 Subject: [PATCH] Update bot_v2.py --- bot_v2.py | 208 +++++++++++++++++++++++++++++------------------------- 1 file changed, 111 insertions(+), 97 deletions(-) diff --git a/bot_v2.py b/bot_v2.py index 624104b..42bd721 100644 --- a/bot_v2.py +++ b/bot_v2.py @@ -4,10 +4,11 @@ Weather Trading Bot v2 — Polymarket Kelly Criterion + Expected Value simulation. Usage: - python weather_bot_v2.py # Paper mode with $1000 virtual balance - python weather_bot_v2.py --live # Real trades - python weather_bot_v2.py --positions - python weather_bot_v2.py --reset # Reset simulation balance + python bot_v2.py # Paper mode with $1000 virtual balance + python bot_v2.py --live # Simulate trades against real prices + python bot_v2.py --positions + python bot_v2.py --reset # Reset simulation balance + python bot_v2.py --monitor # Live price monitor, updates dashboard every 10s """ import re @@ -31,22 +32,40 @@ PRICE_DROP_SIGNAL = _cfg.get("price_drop_threshold", 0.10) # Kelly + EV settings NOAA_ACCURACY = 0.78 # NOAA forecast accuracy for 1-3 day predictions -KELLY_FRACTION = 0.25 # Use 1/4 Kelly for safety (full Kelly is too aggressive) +KELLY_FRACTION = 0.25 # Use 1/4 Kelly for safety MAX_POSITION_PCT = 0.10 # Never bet more than 10% of balance on one trade -MIN_EV = 0.05 # Minimum EV to enter (5 cents per dollar risked) +MIN_EV = 0.05 # Minimum EV to enter SIM_BALANCE = 1000.0 # Starting virtual balance +# Airport coordinates — match the exact stations Polymarket resolves on LOCATIONS = { - "NYC": {"lat": 40.77, "lon": -73.87, "name": "New York City"}, - "Chicago": {"lat": 41.97, "lon": -87.90, "name": "Chicago"}, - "Seattle": {"lat": 47.45, "lon": -122.30, "name": "Seattle"}, - "Atlanta": {"lat": 33.64, "lon": -84.43, "name": "Atlanta"}, - "Dallas": {"lat": 32.90, "lon": -97.04, "name": "Dallas"}, - "Miami": {"lat": 25.80, "lon": -80.29, "name": "Miami"}, + "nyc": {"lat": 40.7772, "lon": -73.8726, "name": "New York City"}, # KLGA LaGuardia + "chicago": {"lat": 41.9742, "lon": -87.9073, "name": "Chicago"}, # KORD O'Hare + "miami": {"lat": 25.7959, "lon": -80.2870, "name": "Miami"}, # KMIA + "dallas": {"lat": 32.8471, "lon": -96.8518, "name": "Dallas"}, # KDAL Love Field + "seattle": {"lat": 47.4502, "lon": -122.3088, "name": "Seattle"}, # KSEA Sea-Tac + "atlanta": {"lat": 33.6407, "lon": -84.4277, "name": "Atlanta"}, # KATL Hartsfield } -ACTIVE_LOCATIONS = _cfg.get("locations", "NYC,Chicago,Seattle,Atlanta,Dallas,Miami").split(",") -ACTIVE_LOCATIONS = [l.strip() for l in ACTIVE_LOCATIONS] +# NWS hourly endpoints per city +NWS_ENDPOINTS = { + "nyc": "https://api.weather.gov/gridpoints/OKX/37,39/forecast/hourly", + "chicago": "https://api.weather.gov/gridpoints/LOT/66,77/forecast/hourly", + "miami": "https://api.weather.gov/gridpoints/MFL/106,51/forecast/hourly", + "dallas": "https://api.weather.gov/gridpoints/FWD/87,107/forecast/hourly", + "seattle": "https://api.weather.gov/gridpoints/SEW/124,61/forecast/hourly", + "atlanta": "https://api.weather.gov/gridpoints/FFC/50,82/forecast/hourly", +} + +# Station IDs for real observations +STATION_IDS = { + "nyc": "KLGA", "chicago": "KORD", "miami": "KMIA", + "dallas": "KDAL", "seattle": "KSEA", "atlanta": "KATL", +} + +ACTIVE_LOCATIONS = _cfg.get("locations", "nyc,chicago,miami,dallas,seattle,atlanta").split(",") +ACTIVE_LOCATIONS = [l.strip().lower() for l in ACTIVE_LOCATIONS] + MONTHS = ["january","february","march","april","may","june", "july","august","september","october","november","december"] @@ -75,12 +94,12 @@ def skip(msg): print(f"{C.GRAY} ⏸️ {msg}{C.RESET}") def calculate_ev(our_prob: float, market_price: float) -> float: """ Expected Value per $1 risked. - EV = (our_prob * payout) - (1 - our_prob) * 1 - payout = (1 / market_price) - 1 (net profit per $1 if we win) - + EV = (our_prob * payout) - (1 - our_prob) + payout = (1 / market_price) - 1 + Example: our_prob=0.75, price=0.08 payout = 1/0.08 - 1 = 11.5x - EV = 0.75 * 11.5 - 0.25 = 8.375 - 0.25 = +$8.12 per $1 risked + EV = 0.75 * 11.5 - 0.25 = +$8.12 per $1 risked """ if market_price <= 0 or market_price >= 1: return 0.0 @@ -93,28 +112,23 @@ def calculate_kelly(our_prob: float, market_price: float) -> float: """ Kelly Criterion: optimal fraction of bankroll to bet. f* = (p * b - q) / b - where: - p = our probability of winning - q = 1 - p (probability of losing) - b = net odds (payout per $1 bet) - + We apply KELLY_FRACTION (0.25) for safety — fractional Kelly. Result is capped at MAX_POSITION_PCT (10% of balance). """ if market_price <= 0 or market_price >= 1: return 0.0 - b = (1.0 / market_price) - 1.0 # net odds + b = (1.0 / market_price) - 1.0 p = our_prob q = 1.0 - p kelly = (p * b - q) / b - kelly = max(0.0, kelly) # never negative - kelly = kelly * KELLY_FRACTION # fractional Kelly - kelly = min(kelly, MAX_POSITION_PCT) # cap at max position + kelly = max(0.0, kelly) + kelly = kelly * KELLY_FRACTION + kelly = min(kelly, MAX_POSITION_PCT) return round(kelly, 4) def calculate_position_size(kelly_fraction: float, balance: float) -> float: - """Convert Kelly fraction to dollar amount.""" return round(kelly_fraction * balance, 2) # ============================================================================= @@ -147,38 +161,60 @@ def reset_sim(): import os if os.path.exists(SIM_FILE): os.remove(SIM_FILE) - if os.path.exists("positions.json"): - os.remove("positions.json") print(f"{C.GREEN} ✅ Simulation reset — balance back to ${SIM_BALANCE:.2f}{C.RESET}") # ============================================================================= -# OPEN-METEO FORECAST +# NWS FORECAST # ============================================================================= -def get_forecast(location: str) -> dict: - loc = LOCATIONS[location] - url = ( - f"https://api.open-meteo.com/v1/forecast" - f"?latitude={loc['lat']}&longitude={loc['lon']}" - f"&daily=temperature_2m_max&temperature_unit=fahrenheit&forecast_days=4" - ) +def get_forecast(city_slug: str) -> dict: + """ + Fetch daily max temperature from NWS. + Combines real station observations (past hours today) with + hourly forecast (upcoming hours) to get the true daily maximum. + """ + forecast_url = NWS_ENDPOINTS.get(city_slug) + station_id = STATION_IDS.get(city_slug) + daily_max = {} + headers = {"User-Agent": "weatherbot/1.0"} + + # Real observations — what already happened today try: - r = requests.get(url, timeout=10) - data = r.json() - result = {} - for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]): - result[date] = round(temp, 1) - return result + obs_url = f"https://api.weather.gov/stations/{station_id}/observations?limit=48" + r = requests.get(obs_url, timeout=10, headers=headers) + for obs in r.json().get("features", []): + props = obs["properties"] + time_str = props.get("timestamp", "")[:10] + temp_c = props.get("temperature", {}).get("value") + if temp_c is not None: + temp_f = round(temp_c * 9/5 + 32) + if time_str not in daily_max or temp_f > daily_max[time_str]: + daily_max[time_str] = temp_f except Exception as e: - warn(f"Forecast error for {location}: {e}") - return {} + warn(f"Observations error for {city_slug}: {e}") + + # Hourly forecast — upcoming hours + try: + r = requests.get(forecast_url, timeout=10, headers=headers) + periods = r.json()["properties"]["periods"] + for p in periods: + date = p["startTime"][:10] + temp = p["temperature"] + if p.get("temperatureUnit") == "C": + temp = round(temp * 9/5 + 32) + if date not in daily_max or temp > daily_max[date]: + daily_max[date] = temp + except Exception as e: + warn(f"Forecast error for {city_slug}: {e}") + + return daily_max # ============================================================================= # POLYMARKET API # ============================================================================= -def get_polymarket_event(location_slug: str, month: str, day: int, year: int) -> dict: - slug = f"highest-temperature-in-{location_slug}-on-{month}-{day}-{year}" +def get_polymarket_event(city_slug: str, month: str, day: int, year: int) -> dict: + slug = f"highest-temperature-in-{city_slug}-on-{month}-{day}-{year}" url = f"https://gamma-api.polymarket.com/events?slug={slug}" try: r = requests.get(url, timeout=10) @@ -214,9 +250,6 @@ def parse_temp_range(question: str) -> tuple: if m: return (int(m.group(1)), int(m.group(2))) return None -def temp_in_range(temp: float, rng: tuple) -> bool: - return rng[0] <= temp <= rng[1] - def hours_until_resolution(event: dict) -> float: try: end_date = event.get("endDate") or event.get("end_date_iso") @@ -254,7 +287,8 @@ def show_positions(): try: url = f"https://gamma-api.polymarket.com/markets/{mid}" r = requests.get(url, timeout=5) - current_price = float(r.json().get("outcomePrices", ["0.5"])[0]) + prices = json.loads(r.json().get("outcomePrices", "[0.5,0.5]")) + current_price = float(prices[0]) except Exception: current_price = pos["entry_price"] @@ -264,12 +298,11 @@ def show_positions(): print(f"\n • {pos['question'][:65]}...") print(f" Entry: ${pos['entry_price']:.3f} | Now: ${current_price:.3f} | " f"Shares: {pos['shares']:.1f} | PnL: {pnl_str}") - print(f" Kelly used: {pos['kelly_pct']:.1%} | EV: {pos['ev']:.2f} | Cost: ${pos['cost']:.2f}") + print(f" Kelly used: {pos.get('kelly_pct', 0):.1%} | EV: {pos.get('ev', 0):.2f} | Cost: ${pos['cost']:.2f}") - balance_str = f"${sim['balance']:.2f}" + print(f"\n Balance: ${sim['balance']:.2f}") pnl_color = C.GREEN if total_pnl >= 0 else C.RED - print(f"\n Balance: {balance_str}") - print(f" Open PnL: {pnl_color}{'+'if total_pnl>=0 else ''}{total_pnl:.2f}{C.RESET}") + print(f" Open PnL: {pnl_color}{'+'if total_pnl>=0 else ''}{total_pnl:.2f}{C.RESET}") print(f" Total trades: {sim['total_trades']} | W/L: {sim['wins']}/{sim['losses']}") # ============================================================================= @@ -284,7 +317,7 @@ def run(dry_run: bool = True): balance = sim["balance"] positions = sim["positions"] - mode = f"{C.YELLOW}PAPER MODE{C.RESET}" if dry_run else f"{C.RED}LIVE MODE{C.RESET}" + mode = f"{C.YELLOW}PAPER MODE{C.RESET}" if dry_run else f"{C.GREEN}LIVE MODE{C.RESET}" starting = sim["starting_balance"] total_return = (balance - starting) / starting * 100 return_str = f"{C.GREEN}+{total_return:.1f}%{C.RESET}" if total_return >= 0 else f"{C.RED}{total_return:.1f}%{C.RESET}" @@ -308,7 +341,8 @@ def run(dry_run: bool = True): try: url = f"https://gamma-api.polymarket.com/markets/{mid}" r = requests.get(url, timeout=5) - current_price = float(r.json().get("outcomePrices", ["0.5"])[0]) + prices = json.loads(r.json().get("outcomePrices", "[0.5,0.5]")) + current_price = float(prices[0]) except Exception: continue @@ -329,7 +363,7 @@ def run(dry_run: bool = True): "closed_at": datetime.now().isoformat(), }) del positions[mid] - ok(f"Closed position — PnL: {'+'if pnl>=0 else ''}{pnl:.2f}") + ok(f"Closed — PnL: {'+'if pnl>=0 else ''}{pnl:.2f}") else: skip("Paper mode — not selling") @@ -339,23 +373,21 @@ def run(dry_run: bool = True): # Scan entries print(f"\n{C.BOLD}🔍 Scanning for entry signals...{C.RESET}") - for loc_key in ACTIVE_LOCATIONS: - loc_key = loc_key.strip() - if loc_key not in LOCATIONS: - warn(f"Unknown location: {loc_key}") + for city_slug in ACTIVE_LOCATIONS: + if city_slug not in LOCATIONS: + warn(f"Unknown location: {city_slug}") continue - loc_data = LOCATIONS[loc_key] - loc_slug = loc_key.lower().replace(" ", "-") + loc_data = LOCATIONS[city_slug] - if loc_key not in forecast_cache: - forecast_cache[loc_key] = get_forecast(loc_key) + if city_slug not in forecast_cache: + forecast_cache[city_slug] = get_forecast(city_slug) - forecast = forecast_cache[loc_key] + forecast = forecast_cache[city_slug] if not forecast: continue - for i in range(0, 3): + for i in range(0, 4): date = datetime.now() + timedelta(days=i) date_str = date.strftime("%Y-%m-%d") month = MONTHS[date.month - 1] @@ -366,7 +398,7 @@ def run(dry_run: bool = True): if forecast_temp is None: continue - event = get_polymarket_event(loc_slug, month, day, year) + event = get_polymarket_event(city_slug, month, day, year) if not event: continue @@ -379,12 +411,11 @@ def run(dry_run: bool = True): skip(f"Resolves in {hours_left:.0f}h — too soon") continue - # Find matching bucket matched = None for market in event.get("markets", []): question = market.get("question", "") rng = parse_temp_range(question) - if rng and temp_in_range(forecast_temp, rng): + if rng and rng[0] <= forecast_temp <= rng[1]: try: prices = json.loads(market.get("outcomePrices", "[0.5,0.5]")) yes_price = float(prices[0]) @@ -411,10 +442,8 @@ def run(dry_run: bool = True): if trend["dropped"]: info(f"📉 Price dropped {abs(trend['change']):.0%} in 24h — stronger signal") - # ── KELLY + EV CALCULATION ── - our_prob = NOAA_ACCURACY # base accuracy - - # Boost if strong trend signal + # Kelly + EV + our_prob = NOAA_ACCURACY if trend["dropped"] and abs(trend["change"]) > 0.20: our_prob = min(0.90, our_prob + 0.05) @@ -432,7 +461,6 @@ def run(dry_run: bool = True): print(f" {C.CYAN} Kelly fraction: {kelly_pct:.1%} of balance{C.RESET}") print(f" {C.CYAN} Position size: ${position_size:.2f}{C.RESET}") - # Entry checks if price >= ENTRY_THRESHOLD: skip(f"Price ${price:.3f} above threshold ${ENTRY_THRESHOLD:.2f}") continue @@ -474,7 +502,7 @@ def run(dry_run: bool = True): "ev": ev, "our_prob": our_prob, "date": date_str, - "location": loc_key, + "location": city_slug, "forecast_temp": forecast_temp, "opened_at": datetime.now().isoformat(), } @@ -492,14 +520,12 @@ def run(dry_run: bool = True): skip("Paper mode — not buying") trades_executed += 1 - # Save simulation state if not dry_run: sim["balance"] = round(balance, 2) sim["positions"] = positions sim["peak_balance"] = max(sim["peak_balance"], balance) save_sim(sim) - # Summary print(f"\n{'=' * 55}") print(f"{C.BOLD}📊 Summary:{C.RESET}") info(f"Opportunities found: {opportunities}") @@ -511,22 +537,18 @@ def run(dry_run: bool = True): print(f"\n {C.YELLOW}[PAPER MODE — use --live to simulate trades against real prices]{C.RESET}") # ============================================================================= -# LIVE MONITOR — updates prices every N seconds, auto-exits on threshold +# LIVE MONITOR # ============================================================================= import time as _time def monitor(interval: int = 10): """ - Background monitor — fetches live prices from Polymarket every N seconds, - updates PnL in simulation.json so the dashboard stays current. + Background monitor — fetches live prices every N seconds, + updates simulation.json so the dashboard stays current. Auto-exits positions when price hits EXIT_THRESHOLD. - - Run: python polymarket_weather_bot.py --monitor - Stop: Ctrl+C """ print(f"\n{C.BOLD}{C.CYAN}📡 Live Monitor — refreshing every {interval}s{C.RESET}") - print(f" Dashboard will update automatically") print(f" Auto-exit threshold: ${EXIT_THRESHOLD:.2f}") print(f" Press Ctrl+C to stop\n") @@ -543,7 +565,6 @@ def monitor(interval: int = 10): total_pnl = 0 for mid, pos in list(positions.items()): - # Fetch current price from Polymarket try: url = f"https://gamma-api.polymarket.com/markets/{mid}" r = requests.get(url, timeout=5) @@ -563,7 +584,6 @@ def monitor(interval: int = 10): f"{pos['question'][:45]}... " f"${current_price:.3f} {pnl_str}") - # Auto-exit if price hit threshold if current_price >= EXIT_THRESHOLD: ok(f"AUTO EXIT: {pos['question'][:50]}... PnL: +${pnl:.2f}") sim["balance"] = round(sim["balance"] + pos["cost"] + pnl, 2) @@ -578,14 +598,10 @@ def monitor(interval: int = 10): "cost": pos["cost"], "kelly_pct": pos.get("kelly_pct", 0), "ev": pos.get("ev", 0), - "location": pos.get("location", ""), - "date": pos.get("date", ""), - "our_prob": pos.get("our_prob", 0), "closed_at": datetime.now().isoformat(), }) del sim["positions"][mid] - sim["positions"] = {k: v for k, v in sim["positions"].items()} sim["peak_balance"] = max(sim.get("peak_balance", sim["balance"]), sim["balance"]) total_str = f"{C.GREEN}+${total_pnl:.2f}{C.RESET}" if total_pnl >= 0 else f"{C.RED}-${abs(total_pnl):.2f}{C.RESET}" @@ -603,7 +619,6 @@ def monitor(interval: int = 10): _time.sleep(interval) - # ============================================================================= # CLI # ============================================================================= @@ -613,8 +628,8 @@ if __name__ == "__main__": parser.add_argument("--live", action="store_true", help="Execute trades (updates simulation balance)") parser.add_argument("--positions", action="store_true", help="Show open positions") parser.add_argument("--reset", action="store_true", help="Reset simulation to $1000") - parser.add_argument("--monitor", action="store_true", help="Live price monitor — updates dashboard every 10s") - parser.add_argument("--interval", type=int, default=10, help="Monitor refresh interval in seconds (default: 10)") + parser.add_argument("--monitor", action="store_true", help="Live price monitor") + parser.add_argument("--interval", type=int, default=10, help="Monitor refresh interval in seconds") args = parser.parse_args() if args.reset: @@ -624,5 +639,4 @@ if __name__ == "__main__": elif args.monitor: monitor(interval=args.interval) else: - run(dry_run=not args.live)