#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Weather Trading Bot v3 — Polymarket CLOB Real Trading ====================================================== bot_v2 strategy logic + py_clob_client on-chain order execution. Only trades US cities (F) for now — EU/Asia cities need CLOB market support. Usage: python bot_v3.py run # Full trading loop (scan + monitor) python bot_v3.py scan # One-shot scan + trade signals python bot_v3.py status # Show open positions + balance python bot_v3.py cancel # Cancel all open orders python bot_v3.py cancel --market # Cancel orders for a market """ import re import sys import json import math import time import os import dotenv from datetime import datetime, timezone, timedelta from pathlib import Path # ============================================================================= # CONFIG # ============================================================================= BOT_DIR = Path(__file__).parent dotenv.load_dotenv(BOT_DIR / ".env") with open(BOT_DIR / "config.json", encoding="utf-8") as f: _cfg = json.load(f) # --- Wallet --- PK = os.getenv("PK", "") WALLET = os.getenv("WALLET", "") SIG_TYPE = int(os.getenv("SIG_TYPE", "0")) # --- Trading --- MAX_BET = _cfg.get("max_bet", 2.0) MIN_EV = _cfg.get("min_ev", 0.10) MAX_PRICE = _cfg.get("max_price", 0.45) MIN_VOLUME = _cfg.get("min_volume", 500) MIN_HOURS = _cfg.get("min_hours", 2.0) MAX_HOURS = _cfg.get("max_hours", 72.0) KELLY_FRAC = _cfg.get("kelly_fraction", 0.25) MAX_SLIPPAGE = _cfg.get("max_slippage", 0.03) SCAN_INTERVAL = _cfg.get("scan_interval", 3600) VC_KEY = _cfg.get("vc_key", "") # --- CLOB --- CLOB_HOST = "https://clob.polymarket.com" CHAIN_ID = 137 # Polygon # --- Contract addresses (Polygon) --- USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" CTF_EXCHANGE = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" NEG_RISK_EXCHANGE = "0xC5d563A36AE78145C45a50134d48A1215220f80a" ROUTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296" CONDITIONAL_TOKENS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" # --- Gas --- MAX_FEE_PER_GAS = 200e9 # 200 gwei # ============================================================================= # MATH # ============================================================================= def norm_cdf(x): return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) def bucket_prob(forecast, t_low, t_high, sigma=2.0): if t_low == -999: return norm_cdf((t_high - float(forecast)) / sigma) if t_high == 999: return 1.0 - norm_cdf((t_low - float(forecast)) / sigma) return 1.0 if in_bucket(forecast, t_low, t_high) else 0.0 def calc_ev(p, price): if price <= 0 or price >= 1: return 0.0 return round(p * (1.0 / price - 1.0) - (1.0 - p), 4) def calc_kelly(p, price): if price <= 0 or price >= 1: return 0.0 b = 1.0 / price - 1.0 f = (p * b - (1.0 - p)) / b return round(min(max(0.0, f) * KELLY_FRAC, 1.0), 4) def bet_size(kelly, balance): raw = kelly * balance return round(min(raw, MAX_BET), 2) # ============================================================================= # COLORS # ============================================================================= class C: GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" CYAN = "\033[96m" GRAY = "\033[90m" RESET = "\033[0m" BOLD = "\033[1m" def ok(msg): print(f"{C.GREEN} ✅ {msg}{C.RESET}") def warn(msg): print(f"{C.YELLOW} ⚠️ {msg}{C.RESET}") def info(msg): print(f"{C.CYAN} {msg}{C.RESET}") def skip(msg): print(f"{C.GRAY} ⏸️ {msg}{C.RESET}") def live(msg): print(f"{C.GREEN} {msg}{C.RESET}") # ============================================================================= # CLOB CLIENT # ============================================================================= from py_clob_client.client import ClobClient from py_clob_client.clob_types import OrderArgs, MarketOrderArgs, OrderType _clob: ClobClient = None def get_clob() -> ClobClient: global _clob if _clob is None: _clob = ClobClient( host=CLOB_HOST, chain_id=CHAIN_ID, key=PK, ) return _clob # ============================================================================= # ON-CHAIN HELPERS # ============================================================================= from web3 import Web3 from eth_account import Account _w3: Web3 = None def get_w3() -> Web3: global _w3 if _w3 is None: _w3 = Web3(Web3.HTTPProvider("https://1rpc.io/matic")) return _w3 def get_nonce(wallet: str) -> int: return get_w3().eth.get_transaction_count(wallet) def send_tx(w3, signed_txn): return w3.eth.send_raw_transaction(signed_txn).hex() def wait_for_receipt(w3, tx_hash: str, timeout=120): start = time.time() while time.time() - start < timeout: try: receipt = w3.eth.get_transaction_receipt(tx_hash) if receipt and receipt["status"] == 1: return receipt except Exception: pass time.sleep(2) return None # ============================================================================= # BALANCE CHECK # ============================================================================= def get_usdc_balance(wallet: str) -> float: """Get USDC.e balance on Polygon.""" w3 = get_w3() usdc_abi = [ { "name": "balanceOf", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function" }, { "name": "decimals", "inputs": [], "outputs": [{"name": "", "type": "uint8"}], "stateMutability": "view", "type": "function" } ] usdc = w3.eth.contract( address=Web3.to_checksum_address(USDC_ADDRESS), abi=usdc_abi ) try: decimals = usdc.functions.decimals().call() bal = usdc.functions.balanceOf(Web3.to_checksum_address(wallet)).call() return bal / (10 ** decimals) except Exception as e: warn(f"Balance check failed: {e}") return 0.0 def get_pol_balance(wallet: str) -> float: w3 = get_w3() bal = w3.eth.get_balance(Web3.to_checksum_address(wallet)) return int(bal) / 1e18 # ============================================================================= # APPROVAL CHECK # ============================================================================= def is_approved(token: str, spender: str, wallet: str) -> bool: """Check if spender is approved for token (USDC.e).""" w3 = get_w3() usdc_abi = [ { "name": "allowance", "inputs": [ {"name": "owner", "type": "address"}, {"name": "spender", "type": "address"} ], "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function" } ] usdc = w3.eth.contract( address=Web3.to_checksum_address(token), abi=usdc_abi ) try: allowance = usdc.functions.allowance( Web3.to_checksum_address(wallet), Web3.to_checksum_address(spender) ).call() return allowance > 0 except Exception: return False def approve_token(token: str, spender: str, wallet: str, private_key: str, amount_wei: int = 2**256 - 1, max_fee: int = MAX_FEE_PER_GAS): """Approve spender to spend token on behalf of wallet.""" w3 = get_w3() usdc_abi = [ { "name": "approve", "inputs": [ {"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"} ], "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function" } ] usdc = w3.eth.contract( address=Web3.to_checksum_address(token), abi=usdc_abi ) nonce = get_nonce(wallet) build = usdc.functions.approve( Web3.to_checksum_address(spender), amount_wei ).build_transaction({ "from": wallet, "nonce": nonce, "maxFeePerGas": max_fee, "maxPriorityFeePerGas": 25e9, "chainId": CHAIN_ID, }) signed = w3.eth.account.sign_transaction(build, private_key) tx_hash = send_tx(w3, signed.raw_transaction) live(f"Approve tx: {tx_hash}") receipt = wait_for_receipt(w3, tx_hash) if receipt: ok(f"Approved {spender} for {token[:10]}...") return True warn(f"Approval tx failed: {tx_hash}") return False def ensure_approvals(): """Ensure all required approvals are set before trading.""" wallet = WALLET required = [ (USDC_ADDRESS, CTF_EXCHANGE), (USDC_ADDRESS, NEG_RISK_EXCHANGE), (USDC_ADDRESS, ROUTER), ] for token, spender in required: if not is_approved(token, spender, wallet): warn(f"Missing approval: {spender[:10]} for {token[:10]}") ok(f"Approving {spender[:10]}...") approve_token(token, spender, wallet, PK) time.sleep(5) # Wait for confirmation else: ok(f"Already approved: {spender[:10]}") # ============================================================================= # ORDER EXECUTION # ============================================================================= def place_buy_order(market_id: str, token_id: str, price: float, shares: float, balance: float, private_key: str, wallet: str) -> dict: """ Place a BUY order on Polymarket CLOB. Uses FOK (Fill-Or-Kill) market order to guarantee execution. Returns dict with success status and details. """ w3 = get_w3() clob = get_clob() cost = round(shares * price, 4) if cost > balance: return {"success": False, "reason": f"Insufficient balance (${balance:.2f} < ${cost:.2f})"} if not is_approved(USDC_ADDRESS, ROUTER, wallet): return {"success": False, "reason": "Router approval missing"} # --- Market order via CLOB --- order_args = MarketOrderArgs( token_id=token_id, amount=cost, # For BUY: amount is in dollars (USDC) side="BUY", price=price, ) try: # First check allowance clob.assert_level_1_auth() order_result = clob.create_market_order(order_args) live(f"Market order placed: {order_result}") except Exception as e: return {"success": False, "reason": f"Order failed: {e}"} return { "success": True, "market_id": market_id, "token_id": token_id, "price": price, "shares": shares, "cost": cost, "order_id": order_result.get("orderID") if isinstance(order_result, dict) else str(order_result), } def cancel_order(order_id: str) -> bool: """Cancel a specific order by ID.""" clob = get_clob() try: clob.cancel(order_id) ok(f"Cancelled order: {order_id[:20]}...") return True except Exception as e: warn(f"Cancel failed: {e}") return False def cancel_all_orders() -> int: """Cancel all open orders. Returns count of cancelled orders.""" clob = get_clob() try: result = clob.cancel_all() count = result.get("count", 0) if isinstance(result, dict) else 0 ok(f"Cancelled {count} orders") return count except Exception as e: warn(f"Cancel all failed: {e}") return 0 # ============================================================================= # LOCATIONS & WEATHER DATA # ============================================================================= LOCATIONS = { "nyc": {"lat": 40.7772, "lon": -73.8726, "name": "New York City", "station": "KLGA", "unit": "F", "region": "us"}, "chicago": {"lat": 41.9742, "lon": -87.9073, "name": "Chicago", "station": "KORD", "unit": "F", "region": "us"}, "miami": {"lat": 25.7959, "lon": -80.2870, "name": "Miami", "station": "KMIA", "unit": "F", "region": "us"}, "dallas": {"lat": 32.8471, "lon": -96.8518, "name": "Dallas", "station": "KDAL", "unit": "F", "region": "us"}, "seattle": {"lat": 47.4502, "lon":-122.3088, "name": "Seattle", "station": "KSEA", "unit": "F", "region": "us"}, "atlanta": {"lat": 33.6407, "lon": -84.4277, "name": "Atlanta", "station": "KATL", "unit": "F", "region": "us"}, } TIMEZONES = { "nyc": "America/New_York", "chicago": "America/Chicago", "miami": "America/New_York", "dallas": "America/Chicago", "seattle": "America/Los_Angeles", "atlanta": "America/New_York", } MONTHS = ["january","february","march","april","may","june", "july","august","september","october","november","december"] import requests def get_ecmwf(city_slug, dates): """ECMWF via Open-Meteo. Returns dict {date: temp_f}.""" loc = LOCATIONS[city_slug] url = ( f"https://api.open-meteo.com/v1/forecast" f"?latitude={loc['lat']}&longitude={loc['lon']}" f"&daily=temperature_2m_max&temperature_unit=fahrenheit" f"&forecast_days=7&timezone={TIMEZONES.get(city_slug, 'UTC')}" f"&models=ecmwf_ifs025&bias_correction=true" ) result = {} for attempt in range(3): try: data = requests.get(url, timeout=(5, 10)).json() if "error" not in data: for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]): if date in dates and temp is not None: result[date] = round(temp) break except Exception as e: if attempt < 2: time.sleep(2) else: warn(f"ECMWF error for {city_slug}: {e}") return result def get_metar(city_slug): """Current observed temperature from METAR station. D+0 only.""" loc = LOCATIONS[city_slug] try: url = f"https://aviationweather.gov/api/data/metar?ids={loc['station']}&format=json" data = requests.get(url, timeout=(5, 8)).json() if data and isinstance(data, list): temp_c = data[0].get("temp") if temp_c is not None: return round(float(temp_c) * 9/5 + 32) except Exception as e: warn(f"METAR error for {city_slug}: {e}") return None def get_forecast_snapshot(city_slug, dates): """Get best temperature forecast for each date. Returns {date: temp_f}.""" ecmwf = get_ecmwf(city_slug, dates) today = datetime.now(timezone.utc).strftime("%Y-%m-%d") result = {} for date in dates: best = ecmwf.get(date) best_source = "ecmwf" # METAR for today if available if date == today: metar = get_metar(city_slug) if metar is not None: best = metar best_source = "metar" if best is not None: result[date] = {"temp": best, "source": best_source} return result # ============================================================================= # POLYMARKET # ============================================================================= def get_polymarket_event(city_slug, month, day, year): slug = f"highest-temperature-in-{city_slug}-on-{month}-{day}-{year}" try: r = requests.get(f"https://gamma-api.polymarket.com/events?slug={slug}", timeout=(5, 8)) data = r.json() if data and isinstance(data, list) and len(data) > 0: return data[0] except Exception as e: warn(f"Polymarket API error: {e}") return None def get_market_price(market_id): try: r = requests.get(f"https://gamma-api.polymarket.com/markets/{market_id}", timeout=(3, 5)) data = r.json() prices = json.loads(data.get("outcomePrices", "[0.5,0.5]")) return float(prices[0]), float(prices[1]) if len(prices) > 1 else float(prices[0]) except Exception: return None, None def parse_temp_range(question): if not question: return None num = r'(-?\d+(?:\.\d+)?)' if re.search(r'or below', question, re.IGNORECASE): m = re.search(num + r'[°]?[FC] or below', question, re.IGNORECASE) if m: return (-999.0, float(m.group(1))) if re.search(r'or higher', question, re.IGNORECASE): m = re.search(num + r'[°]?[FC] or higher', question, re.IGNORECASE) if m: return (float(m.group(1)), 999.0) m = re.search(r'between ' + num + r'-' + num + r'[°]?[FC]', question, re.IGNORECASE) if m: return (float(m.group(1)), float(m.group(2))) m = re.search(r'be ' + num + r'[°]?[FC] on', question, re.IGNORECASE) if m: v = float(m.group(1)) return (v, v) return None def hours_to_resolution(end_date_str): try: end = datetime.fromisoformat(end_date_str.replace("Z", "+00:00")) return max(0.0, (end - datetime.now(timezone.utc)).total_seconds() / 3600) except Exception: return 999.0 def in_bucket(forecast, t_low, t_high): if t_low == t_high: return round(float(forecast)) == round(t_low) return t_low <= float(forecast) <= t_high def get_condition_id(market_id: str) -> str: """Get condition ID for a market from Polymarket.""" try: r = requests.get(f"https://gamma-api.polymarket.com/markets/{market_id}", timeout=(5, 8)) data = r.json() return data.get("conditionId", "") except Exception: return "" # ============================================================================= # STATE (local JSON) # ============================================================================= DATA_DIR = BOT_DIR / "data" DATA_DIR.mkdir(exist_ok=True) MARKETS_DIR = DATA_DIR / "markets" MARKETS_DIR.mkdir(exist_ok=True) STATE_FILE = DATA_DIR / "state_v3.json" def load_state(): if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text(encoding="utf-8")) return { "balance": 0.0, "starting_balance": 0.0, "total_trades": 0, "wins": 0, "losses": 0, "open_orders": {}, } def save_state(state): STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") def market_path(city_slug, date_str): return MARKETS_DIR / f"{city_slug}_{date_str}.json" def load_market(city_slug, date_str): p = market_path(city_slug, date_str) if p.exists(): return json.loads(p.read_text(encoding="utf-8")) return None def save_market(market): p = market_path(market["city"], market["date"]) p.write_text(json.dumps(market, indent=2, ensure_ascii=False), encoding="utf-8") def load_all_markets(): markets = [] for f in MARKETS_DIR.glob("*.json"): try: markets.append(json.loads(f.read_text(encoding="utf-8"))) except Exception: pass return markets # ============================================================================= # SIGMA (weather forecast uncertainty) # ============================================================================= SIGMA_F = 2.0 def get_sigma(city_slug): return SIGMA_F # Flat sigma for now; calibration can be added later # ============================================================================= # OPEN POSITIONS from CLOB # ============================================================================= def get_clob_positions(): """Get all open orders/positions from CLOB.""" clob = get_clob() try: orders = clob.get_orders() return orders if orders else [] except Exception as e: warn(f"Failed to fetch CLOB orders: {e}") return [] # ============================================================================= # SCAN & TRADE (one shot) # ============================================================================= def scan_and_trade(): """ One-shot scan: check all cities for trade signals and execute real orders. Returns (new_trades, errors). """ now = datetime.now(timezone.utc) state = load_state() balance = get_usdc_balance(WALLET) if balance != state.get("balance"): state["balance"] = balance save_state(state) print(f"\n{C.BOLD}{C.CYAN}🌤 Weather Trading Bot v3 — Live Mode{C.RESET}") print("=" * 60) print(f" Wallet: {WALLET[:8]}...{WALLET[-4:]}") print(f" USDC.e: ${balance:.4f}") print(f" POL balance: {get_pol_balance(WALLET):.4f} POL") print(f" Max bet: ${MAX_BET} | Min EV: {MIN_EV*100:.0f}%") print() new_trades = 0 errors = [] for city_slug, loc in LOCATIONS.items(): print(f" -> {loc['name']}...", end=" ", flush=True) unit_sym = "F" try: dates = [(now + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(4)] forecasts = get_forecast_snapshot(city_slug, dates) time.sleep(0.3) except Exception as e: print(f"error ({e})") continue for i, date in enumerate(dates): dt = datetime.strptime(date, "%Y-%m-%d") event = get_polymarket_event( city_slug, MONTHS[dt.month - 1], dt.day, dt.year ) if not event: continue end_date = event.get("endDate", "") hours = hours_to_resolution(end_date) if end_date else 0 horizon = f"D+{i}" if hours < MIN_HOURS or hours > MAX_HOURS: continue # Parse all outcome buckets from Polymarket outcomes = [] for market in event.get("markets", []): question = market.get("question", "") mid = str(market.get("id", "")) volume = float(market.get("volume", 0)) rng = parse_temp_range(question) if not rng: continue try: prices = json.loads(market.get("outcomePrices", "[0.5,0.5]")) bid = float(prices[0]) ask = float(prices[1]) if len(prices) > 1 else bid except Exception: continue outcomes.append({ "question": question, "market_id": mid, "range": rng, "bid": round(bid, 4), "ask": round(ask, 4), "price": round(bid, 4), "spread": round(ask - bid, 4), "volume": round(volume, 0), }) if not outcomes: continue forecastsnap = forecasts.get(date, {}) forecast_temp = forecastsnap.get("temp") best_source = forecastsnap.get("source", "ecmwf") if forecast_temp is None: continue sigma = get_sigma(city_slug) best_signal = None # Find the bucket that matches our forecast for o in outcomes: t_low, t_high = o["range"] if not in_bucket(forecast_temp, t_low, t_high): continue volume = o["volume"] ask = o["ask"] spread = o["spread"] if volume < MIN_VOLUME: continue if ask >= MAX_PRICE: continue if spread > MAX_SLIPPAGE: continue p = bucket_prob(forecast_temp, t_low, t_high, sigma) ev = calc_ev(p, ask) if ev < MIN_EV: continue kelly = calc_kelly(p, ask) size = bet_size(kelly, balance) if size < 0.50: continue shares = round(size / ask, 2) token_id = get_condition_id(o["market_id"]) best_signal = { "market_id": o["market_id"], "token_id": token_id, "question": o["question"], "bucket_low": t_low, "bucket_high": t_high, "entry_price": ask, "bid": o["bid"], "spread": spread, "shares": shares, "cost": round(shares * ask, 4), "p": round(p, 4), "ev": round(ev, 4), "kelly": round(kelly, 4), "forecast_temp": forecast_temp, "forecast_src": best_source, "sigma": sigma, "volume": volume, } break # Only one bucket per market if best_signal: bucket_label = f"{best_signal['bucket_low']}-{best_signal['bucket_high']}{unit_sym}" print(f"\n {C.BOLD}📍 {loc['name']} {horizon} — {date}{C.RESET}") print(f" {C.CYAN} Forecast: {forecast_temp}°F ({best_source}) | {bucket_label}{C.RESET}") print(f" {C.GREEN} ✅ BUY SIGNAL | ${best_signal['cost']:.2f} @ ${ask:.3f} | " f"EV {best_signal['ev']:+.2f} | Kel {best_signal['kelly']:.2f}{C.RESET}") # --- EXECUTE REAL ORDER --- result = place_buy_order( market_id=best_signal["market_id"], token_id=best_signal["token_id"], price=best_signal["entry_price"], shares=best_signal["shares"], balance=balance, private_key=PK, wallet=WALLET, ) if result["success"]: new_trades += 1 state["total_trades"] += 1 balance -= best_signal["cost"] live(f" [LIVE] BUY {loc['name']} {horizon} | {bucket_label} @ ${best_signal['entry_price']:.3f} " f"| EV {best_signal['ev']:+.2f} | ${best_signal['cost']:.2f}") # Save to market record mkt_record = load_market(city_slug, date) or { "city": city_slug, "city_name": loc["name"], "date": date, "unit": "F", "event_end_date": end_date, "status": "open", "position": None, } mkt_record["position"] = { **best_signal, "order_id": result.get("order_id"), "opened_at": datetime.now(timezone.utc).isoformat(), "status": "open", "closed_at": None, "close_reason": None, "exit_price": None, "pnl": None, } save_market(mkt_record) else: errors.append(f"{loc['name']} {horizon}: {result['reason']}") warn(f" ❌ Order failed: {result['reason']}") else: # No signal — show why for o in outcomes: t_low, t_high = o["range"] if not in_bucket(forecast_temp, t_low, t_high): continue ask = o["ask"] p = bucket_prob(forecast_temp, t_low, t_high, sigma) ev = calc_ev(p, ask) skip(f" {forecast_temp}°F bucket {t_low}-{t_high}F @ ${ask:.3f} EV={ev:.2f} — skipped") break print("ok") # Save updated balance state["balance"] = round(balance, 4) save_state(state) print(f"\n{'=' * 60}") print(f" Scanned: {len(LOCATIONS)} cities") print(f" New trades: {C.GREEN}{new_trades}{C.RESET}") print(f" Errors: {len(errors)}") print(f" Balance: ${balance:.4f}") print(f"{'=' * 60}\n") return new_trades, errors # ============================================================================= # STATUS # ============================================================================= def show_status(): """Show current balance, positions, and open orders.""" balance = get_usdc_balance(WALLET) pol_bal = get_pol_balance(WALLET) print(f"\n{C.BOLD}{C.CYAN}📊 Bot v3 — Status{C.RESET}") print("=" * 60) print(f" Wallet: {WALLET[:8]}...{WALLET[-4:]}") print(f" USDC.e: ${balance:.4f}") print(f" POL: {pol_bal:.4f}") print() # Open orders from CLOB orders = get_clob_positions() if orders: print(f" Open orders: {len(orders)}") for o in orders: print(f" {o.get('side','?')} {o.get('size','?')} @ ${o.get('price','?')} " f"[{o.get('marketID','')[:16]}...]") else: print(f" Open orders: 0") # Local market positions markets = load_all_markets() open_pos = [m for m in markets if m.get("position") and m["position"].get("status") == "open"] if open_pos: print(f"\n Open positions (local): {len(open_pos)}") for m in open_pos: pos = m["position"] unit_sym = "F" label = f"{pos['bucket_low']}-{pos['bucket_high']}{unit_sym}" print(f" {m['city_name']} {m['date']} | {label} | " f"entry ${pos['entry_price']:.3f} | cost ${pos.get('cost',0):.2f}") else: print(f"\n Open positions: 0") print(f"{'=' * 60}\n") # ============================================================================= # MAIN LOOP # ============================================================================= MONITOR_INTERVAL = 600 # 10 minutes def run_loop(): print(f"\n{C.BOLD}{C.CYAN}🌤 Weather Trading Bot v3 — LIVE{C.RESET}") print("=" * 60) print(f" Wallet: {WALLET[:8]}...{WALLET[-4:]}") print(f" Cities: {len(LOCATIONS)}") print(f" Max bet: ${MAX_BET} | Kelly fraction: {KELLY_FRAC}") print(f" Min EV: {MIN_EV*100:.0f}%") print(f" Scan: every {SCAN_INTERVAL//60} min") print(f" Monitor: every {MONITOR_INTERVAL//60} min") print() # Check approvals on startup ok("Checking approvals...") ensure_approvals() last_full_scan = 0 while True: now_ts = time.time() now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if now_ts - last_full_scan >= SCAN_INTERVAL: print(f"[{now_str}] Full scan...") try: new_trades, errors = scan_and_trade() last_full_scan = time.time() except Exception as e: warn(f"Scan error: {e}") time.sleep(60) continue else: print(f"[{now_str}] Monitoring...") time.sleep(MONITOR_INTERVAL) # ============================================================================= # CLI # ============================================================================= if __name__ == "__main__": if not PK or not WALLET: print("ERROR: PK and WALLET must be set in weatherbot/.env") sys.exit(1) cmd = sys.argv[1] if len(sys.argv) > 1 else "scan" if cmd == "run": run_loop() elif cmd == "scan": scan_and_trade() elif cmd == "status": show_status() elif cmd == "cancel": market_id = sys.argv[2] if len(sys.argv) > 2 else None if market_id: print(f"Cancelling orders for market: {market_id}") else: count = cancel_all_orders() print(f"Cancelled {count} orders") else: print(f"Usage: python bot_v3.py [scan|run|status|cancel]")