Update bot_v2.py

This commit is contained in:
alteregoeth-ai
2026-03-22 07:06:05 -05:00
committed by GitHub
parent 137c7ed446
commit d05c077294
+160 -99
View File
@@ -1,15 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
bot_v2.py — Weather Trading Bot for Polymarket weatherbet.py — Weather Trading Bot for Polymarket
===================================================== =====================================================
Tracks weather forecasts from 3 sources (ECMWF, HRRR, METAR), Tracks weather forecasts from 3 sources (ECMWF, HRRR, METAR),
compares with Polymarket markets, paper trades using Kelly criterion. compares with Polymarket markets, paper trades using Kelly criterion.
Usage: Usage:
python bot_v2.py # main loop python weatherbet.py # main loop
python bot_v2.py report # full report python weatherbet.py report # full report
python bot_v2.py status # balance and open positions python weatherbet.py status # balance and open positions
""" """
import re import re
@@ -177,21 +177,26 @@ def get_ecmwf(city_slug, dates):
unit = loc["unit"] unit = loc["unit"]
temp_unit = "fahrenheit" if unit == "F" else "celsius" temp_unit = "fahrenheit" if unit == "F" else "celsius"
result = {} result = {}
try: url = (
url = ( f"https://api.open-meteo.com/v1/forecast"
f"https://api.open-meteo.com/v1/forecast" f"?latitude={loc['lat']}&longitude={loc['lon']}"
f"?latitude={loc['lat']}&longitude={loc['lon']}" f"&daily=temperature_2m_max&temperature_unit={temp_unit}"
f"&daily=temperature_2m_max&temperature_unit={temp_unit}" f"&forecast_days=7&timezone={TIMEZONES.get(city_slug, 'UTC')}"
f"&forecast_days=7&timezone={TIMEZONES.get(city_slug, 'UTC')}" f"&models=ecmwf_ifs025&bias_correction=true"
f"&models=ecmwf_ifs025&bias_correction=true" )
) for attempt in range(3):
data = requests.get(url, timeout=(5, 8)).json() try:
if "error" not in data: data = requests.get(url, timeout=(5, 10)).json()
for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]): if "error" not in data:
if date in dates and temp is not None: for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]):
result[date] = round(temp, 1) if unit == "C" else round(temp) if date in dates and temp is not None:
except Exception as e: result[date] = round(temp, 1) if unit == "C" else round(temp)
print(f" [ECMWF] {city_slug}: {e}") break
except Exception as e:
if attempt < 2:
time.sleep(3)
else:
print(f" [ECMWF] {city_slug}: {e}")
return result return result
def get_hrrr(city_slug, dates): def get_hrrr(city_slug, dates):
@@ -200,21 +205,26 @@ def get_hrrr(city_slug, dates):
if loc["region"] != "us": if loc["region"] != "us":
return {} return {}
result = {} result = {}
try: url = (
url = ( f"https://api.open-meteo.com/v1/forecast"
f"https://api.open-meteo.com/v1/forecast" f"?latitude={loc['lat']}&longitude={loc['lon']}"
f"?latitude={loc['lat']}&longitude={loc['lon']}" f"&daily=temperature_2m_max&temperature_unit=fahrenheit"
f"&daily=temperature_2m_max&temperature_unit=fahrenheit" f"&forecast_days=3&timezone={TIMEZONES.get(city_slug, 'UTC')}"
f"&forecast_days=3&timezone={TIMEZONES.get(city_slug, 'UTC')}" f"&models=gfs_seamless" # HRRR+GFS seamless — best option for US
f"&models=gfs_seamless" # HRRR+GFS seamless — best option for US )
) for attempt in range(3):
data = requests.get(url, timeout=(5, 8)).json() try:
if "error" not in data: data = requests.get(url, timeout=(5, 10)).json()
for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]): if "error" not in data:
if date in dates and temp is not None: for date, temp in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"]):
result[date] = round(temp) if date in dates and temp is not None:
except Exception as e: result[date] = round(temp)
print(f" [HRRR] {city_slug}: {e}") break
except Exception as e:
if attempt < 2:
time.sleep(3)
else:
print(f" [HRRR] {city_slug}: {e}")
return result return result
def get_metar(city_slug): def get_metar(city_slug):
@@ -593,68 +603,86 @@ def scan_and_update():
sigma = get_sigma(city_slug, best_source or "ecmwf") sigma = get_sigma(city_slug, best_source or "ecmwf")
best_signal = None best_signal = None
# Find exactly ONE bucket that matches the forecast
# If forecast doesn't fit any bucket cleanly — skip this market
matched_bucket = None
for o in outcomes: for o in outcomes:
t_low, t_high = o["range"] t_low, t_high = o["range"]
price = o["price"] if in_bucket(forecast_temp, t_low, t_high):
matched_bucket = o
break
if matched_bucket:
o = matched_bucket
t_low, t_high = o["range"]
volume = o["volume"] volume = o["volume"]
if not in_bucket(forecast_temp, t_low, t_high):
continue
bid = o.get("bid", o["price"]) bid = o.get("bid", o["price"])
ask = o.get("ask", o["price"]) ask = o.get("ask", o["price"])
spread = o.get("spread", 0) spread = o.get("spread", 0)
# Slippage filter # All filters — if any fails, skip this market entirely
if spread > MAX_SLIPPAGE: if volume >= MIN_VOLUME:
continue p = bucket_prob(forecast_temp, t_low, t_high, sigma)
if ask >= MAX_PRICE or volume < MIN_VOLUME: ev = calc_ev(p, ask)
continue if ev >= MIN_EV:
kelly = calc_kelly(p, ask)
p = bucket_prob(forecast_temp, t_low, t_high, sigma) size = bet_size(kelly, balance)
ev = calc_ev(p, ask) # EV calculated from ask if size >= 0.50:
if ev < MIN_EV: best_signal = {
continue "market_id": o["market_id"],
"question": o["question"],
kelly = calc_kelly(p, ask) "bucket_low": t_low,
size = bet_size(kelly, balance) "bucket_high": t_high,
if size < 0.50: "entry_price": ask,
continue "bid_at_entry": bid,
"spread": spread,
best_signal = { "shares": round(size / ask, 2),
"market_id": o["market_id"], "cost": size,
"question": o["question"], "p": round(p, 4),
"bucket_low": t_low, "ev": round(ev, 4),
"bucket_high": t_high, "kelly": round(kelly, 4),
"entry_price": ask, # enter at ask "forecast_temp":forecast_temp,
"bid_at_entry": bid, "forecast_src": best_source,
"spread": spread, "sigma": sigma,
"shares": round(size / ask, 2), "opened_at": snap.get("ts"),
"cost": size, "status": "open",
"p": round(p, 4), "pnl": None,
"ev": round(ev, 4), "exit_price": None,
"kelly": round(kelly, 4), "close_reason": None,
"forecast_temp":forecast_temp, "closed_at": None,
"forecast_src": best_source, }
"sigma": sigma,
"opened_at": snap.get("ts"),
"status": "open",
"pnl": None,
"exit_price": None,
"close_reason": None,
"closed_at": None,
}
break
if best_signal: if best_signal:
balance -= best_signal["cost"] # Fetch real bestAsk from Polymarket API for accurate entry price
mkt["position"] = best_signal skip_position = False
state["total_trades"] += 1 try:
new_pos += 1 r = requests.get(f"https://gamma-api.polymarket.com/markets/{best_signal['market_id']}", timeout=(3, 5))
bucket_label = f"{best_signal['bucket_low']}-{best_signal['bucket_high']}{unit_sym}" mdata = r.json()
print(f" [BUY] {loc['name']} {horizon} {date} | {bucket_label} | " real_ask = float(mdata.get("bestAsk", best_signal["entry_price"]))
f"${best_signal['entry_price']:.3f} | EV {best_signal['ev']:+.2f} | " real_bid = float(mdata.get("bestBid", best_signal["bid_at_entry"]))
f"${best_signal['cost']:.2f} ({best_signal['forecast_src'].upper()})") real_spread = round(real_ask - real_bid, 4)
# Re-check slippage and price with real values
if real_spread > MAX_SLIPPAGE or real_ask >= MAX_PRICE:
print(f" [SKIP] {loc['name']} {date} — real ask ${real_ask:.3f} spread ${real_spread:.3f}")
skip_position = True
else:
best_signal["entry_price"] = real_ask
best_signal["bid_at_entry"] = real_bid
best_signal["spread"] = real_spread
best_signal["shares"] = round(best_signal["cost"] / real_ask, 2)
best_signal["ev"] = round(calc_ev(best_signal["p"], real_ask), 4)
except Exception as e:
print(f" [WARN] Could not fetch real ask for {best_signal['market_id']}: {e}")
if not skip_position and best_signal["entry_price"] < MAX_PRICE:
balance -= best_signal["cost"]
mkt["position"] = best_signal
state["total_trades"] += 1
new_pos += 1
bucket_label = f"{best_signal['bucket_low']}-{best_signal['bucket_high']}{unit_sym}"
print(f" [BUY] {loc['name']} {horizon} {date} | {bucket_label} | "
f"${best_signal['entry_price']:.3f} | EV {best_signal['ev']:+.2f} | "
f"${best_signal['cost']:.2f} ({best_signal['forecast_src'].upper()})")
# Market closed by time # Market closed by time
if hours < 0.5 and mkt["status"] == "open": if hours < 0.5 and mkt["status"] == "open":
@@ -846,39 +874,72 @@ def monitor_positions():
pos = mkt["position"] pos = mkt["position"]
mid = pos["market_id"] mid = pos["market_id"]
# Get current price from all_outcomes (no extra requests) # Fetch real bestBid from Polymarket API — actual sell price
current_price = None current_price = None
for o in mkt.get("all_outcomes", []): try:
if o["market_id"] == mid: r = requests.get(f"https://gamma-api.polymarket.com/markets/{mid}", timeout=(3, 5))
current_price = o.get("bid", o["price"]) # use bid — sell price mdata = r.json()
break best_bid = mdata.get("bestBid")
if best_bid is not None:
current_price = float(best_bid)
except Exception:
pass
# Fallback to cached price if API failed
if current_price is None:
for o in mkt.get("all_outcomes", []):
if o["market_id"] == mid:
current_price = o.get("bid", o["price"])
break
if current_price is None: if current_price is None:
continue continue
entry = pos["entry_price"] entry = pos["entry_price"]
stop = pos.get("stop_price", entry * 0.80) stop = pos.get("stop_price", entry * 0.80)
city_name = LOCATIONS.get(mkt["city"], {}).get("name", mkt["city"])
# Hours left to resolution
end_date = mkt.get("event_end_date", "")
hours_left = hours_to_resolution(end_date) if end_date else 999.0
# Take-profit threshold based on hours to resolution
if hours_left < 24:
take_profit = None # hold to resolution
elif hours_left < 48:
take_profit = 0.85 # 24-48h: take profit at $0.85
else:
take_profit = 0.75 # 48h+: take profit at $0.75
# Trailing: if up 20%+ — move stop to breakeven # Trailing: if up 20%+ — move stop to breakeven
if current_price >= entry * 1.20 and stop < entry: if current_price >= entry * 1.20 and stop < entry:
pos["stop_price"] = entry pos["stop_price"] = entry
pos["trailing_activated"] = True pos["trailing_activated"] = True
city_name = LOCATIONS.get(mkt["city"], {}).get("name", mkt["city"])
print(f" [TRAILING] {city_name} {mkt['date']} — stop moved to breakeven ${entry:.3f}") print(f" [TRAILING] {city_name} {mkt['date']} — stop moved to breakeven ${entry:.3f}")
# Check take-profit
take_triggered = take_profit is not None and current_price >= take_profit
# Check stop # Check stop
if current_price <= stop: stop_triggered = current_price <= stop
if take_triggered or stop_triggered:
pnl = round((current_price - entry) * pos["shares"], 2) pnl = round((current_price - entry) * pos["shares"], 2)
balance += pos["cost"] + pnl balance += pos["cost"] + pnl
pos["closed_at"] = datetime.now(timezone.utc).isoformat() pos["closed_at"] = datetime.now(timezone.utc).isoformat()
pos["close_reason"] = "stop_loss" if current_price < entry else "trailing_stop" if take_triggered:
pos["close_reason"] = "take_profit"
reason = "TAKE"
elif current_price < entry:
pos["close_reason"] = "stop_loss"
reason = "STOP"
else:
pos["close_reason"] = "trailing_stop"
reason = "TRAILING BE"
pos["exit_price"] = current_price pos["exit_price"] = current_price
pos["pnl"] = pnl pos["pnl"] = pnl
pos["status"] = "closed" pos["status"] = "closed"
closed += 1 closed += 1
reason = "STOP" if current_price < entry else "TRAILING BE" print(f" [{reason}] {city_name} {mkt['date']} | entry ${entry:.3f} exit ${current_price:.3f} | {hours_left:.0f}h left | PnL: {'+'if pnl>=0 else ''}{pnl:.2f}")
city_name = LOCATIONS.get(mkt["city"], {}).get("name", mkt["city"])
print(f" [{reason}] {city_name} {mkt['date']} | entry ${entry:.3f} exit ${current_price:.3f} | PnL: {'+'if pnl>=0 else ''}{pnl:.2f}")
save_market(mkt) save_market(mkt)
if closed: if closed:
@@ -964,4 +1025,4 @@ if __name__ == "__main__":
_cal = load_cal() _cal = load_cal()
print_report() print_report()
else: else:
print("Usage: python bot_v2.py [run|status|report]") print("Usage: python weatherbet.py [run|status|report]")