"""quad_autopilot.py - paper autopilot for the PolyMarket crypto Up/Down quad. Targets the 15-minute "Up or Down" markets for BTC / ETH / SOL / XRP. Validated edge (fresh 1000-bar OOS, after 3c spread): RSI(14) < 30 -> BUY YES(up), resolved at close[t+3]: hit 0.788, ROI +0.258 (5-min horizon had no edge: hit 0.515, ROI -0.015 -> skipped) Per tick (every 5 min via cron): 1. Binance 5m candles for each coin 2. RSI(14) at last closed candle; RSI < 30 -> oversold candidate 3. Find the live accepting 15-min Up/Down market for that coin (gamma-api) 4. Optional Kronos veto (--kronos): skip if P(up) < 0.10 5. Log paper BUY_YES + projected resolve to the ledger CSV; push ledger to HF bucket PAPER ONLY: no real orders. Ledger rows are marked mode=paper. """ import argparse import csv import json import os import subprocess import sys import urllib.request from datetime import datetime, timezone COINS = ["BTC", "ETH", "SOL", "XRP"] # Tradeable edge verified 2026-08-09 on 21 days x 4 coins (6048x5m bars): # only ETH, 15-min products, RSI(14)<30, buy YES-up (+15m), breakeven 0.53. # hit 0.609 (n=64), edge +0.079, walk-forward OOS 0.680. BTC/SOL/XRP = coin-flip. # EDGE_COINS controls which coins may fire BUY_YES; "" = all (research mode). EDGE_COINS = os.environ.get("EDGE_COINS", "ETH").split(",") EDGE_THRESHOLD = float(os.environ.get("EDGE_THRESHOLD", "30")) BINANCE_HOSTS = os.environ.get("BINANCE_HOSTS", "data-api.binance.vision,api.binance.com").split(",") BINANCE_TMPL = "https://{}/api/v3/klines?symbol={}USDT&interval=5m&limit=1200" GAMMA = "https://gamma-api.polymarket.com/markets?limit=100&closed=false&order=updatedAt&ascending=false&offset=0" LEDGER = os.environ.get("LEDGER_PATH", "/home/neo/quad_ledger.csv") HF_REPO = "40Hz/kronos-data" HF_CMD = os.environ.get("HF_CMD", "hf") FIELDS = ["ts", "mode", "coin", "market_id", "question", "rsi14", "side", "action", "yes_price", "resolve_ts", "kronos_prob_up", "kronos_veto", "note"] def fetch_closes(symbol): for host in BINANCE_HOSTS: host = host.strip() url = BINANCE_TMPL.format(host, symbol) try: with urllib.request.urlopen(url, timeout=15) as r: arr = json.load(r) opens = [float(x[1]) for x in arr] closes = [float(x[4]) for x in arr] ts = [int(x[0]) for x in arr] return opens, closes, ts except Exception as e: print(f"[{symbol}] {host} failed ({e}); trying next host") continue raise RuntimeError("all binance hosts failed") def agg_bars(opens, closes, ts, bucket_min=15): """Aggregate 5m bars to bucket_min closes (floor to bucket; last close wins).""" buckets = {} for i in range(len(ts)): b = i // (bucket_min // 5) buckets[b] = closes[i] return [buckets[k] for k in sorted(buckets)] def rsi(closes, period=14): d = [closes[i] - closes[i - 1] for i in range(1, len(closes))] gains = [x if x > 0 else 0.0 for x in d] losses = [-x if x < 0 else 0.0 for x in d] ag = sum(gains[:period]) / period al = sum(losses[:period]) / period out = [] for i in range(len(d)): if i >= period: ag = (ag * (period - 1) + gains[i]) / period al = (al * (period - 1) + losses[i]) / period out.append(100 if al == 0 else 100 - 100 / (1 + ag / al)) return out def now_iso(): return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def find_updown_markets(): """Return list of (market) for 'Up or Down' markets still accepting, grouped by coin.""" req = urllib.request.Request(GAMMA, headers={"User-Agent": "quad-lab/0.1"}) with urllib.request.urlopen(req, timeout=60) as r: markets = json.load(r) out = [] for m in markets: q = m.get("question") or "" if "up or down" not in q.lower(): continue if not m.get("acceptingOrders"): continue out.append(m) return out def pick_market(markets, coin, max_mins=20): """Choose the coin's 15-min Up/Down market that is still accepting and closes soonest.""" now = datetime.now(timezone.utc) best = None for m in markets: q = m.get("question") or "" if not q.lower().startswith(coin + " up or down"): continue try: end = datetime.fromisoformat(m["endDate"].replace("Z", "+00:00")) except Exception: continue mins = (end - now).total_seconds() / 60 if mins <= 0 or mins > max_mins: continue if best is None or mins < best[0]: best = (mins, m) return best def kronos_veto(coin, prob_cache=None): """Optional GPU veto: skip if Kronos strongly DOWN (P(up) < 0.1). Returns (prob_up, veto). If unavailable -> (None, False).""" if prob_cache is None: return None, False prob = prob_cache.get(coin) if prob is None: return None, False return prob, prob < 0.10 def load_ledger(): rows = [] if os.path.exists(LEDGER): with open(LEDGER) as f: rd = csv.DictReader(f) rows = [r for r in rd] return rows def append_ledger(row): exists = os.path.exists(LEDGER) with open(LEDGER, "a", newline="") as f: w = csv.DictWriter(f, fieldnames=FIELDS) if not exists: w.writeheader() w.writerow({k: row.get(k, "") for k in FIELDS}) def push_ledger(): try: subprocess.run([HF_CMD, "upload", HF_REPO, LEDGER, "--commit-message", "quad autopilot ledger " + now_iso()], check=True, capture_output=True, timeout=120) print(" ledger pushed to", HF_REPO) except Exception as e: print(" ledger push skipped:", e) def main(): ap = argparse.ArgumentParser() ap.add_argument("--kronos", action="store_true", help="enable Kronos veto (calls GPU Space)") ap.add_argument("--push", action="store_true", help="push ledger to HF bucket") ap.add_argument("--coin", choices=COINS + ["ALL"], default="ALL") args = ap.parse_args() coins = COINS if args.coin == "ALL" else [args.coin] markets = [] if not args.kronos: try: markets = find_updown_markets() except Exception as e: print("market scan skipped:", e) prob_cache = {} # filled only when --kronos implemented lines = [] for coin in coins: try: opens, closes, ts = fetch_closes(coin) except Exception as e: print(f"[{coin}] fetch fail: {e}") continue rs5 = rsi(closes) rs15 = rsi(agg_bars(opens, closes, ts, 15)) last_r5 = rs5[-1] last_r15 = rs15[-1] price = closes[-1] # Edge verified on 15-min RSI. Require the coin to be edge-enabled. edge_ok = coin in [c.strip().upper() for c in EDGE_COINS] trigger = last_r15 < EDGE_THRESHOLD and edge_ok prob, veto = kronos_veto(coin, prob_cache if args.kronos else None) market_id = question = yes_price = resolve_ts = "" if trigger and not veto: if markets: hit = pick_market(markets, coin) if hit: mins, m = hit market_id = m["id"] question = m["question"] yes_price = (m.get("outcomePrices") or "?")[:60] resolve_ts = m["endDate"] action = "BUY_YES_15M_UP" if market_id else "BUY_YES_15M_UP_NO_MARKET" elif trigger and veto: action = "VETO" else: action = "NO_TRADE" note = (f"price={price:.2f} rsi15={last_r15:.1f} rsi5={last_r5:.1f} " f"trig={trigger} edge_ok={edge_ok} " + (f"kronos={prob:.3f} veto={veto} " if prob is not None else "") + ("market=" + market_id if market_id else "")) row = {"ts": now_iso(), "mode": "paper", "coin": coin, "market_id": market_id, "question": question, "rsi14": round(last_r15, 2), "side": "YES", "action": action, "yes_price": yes_price, "resolve_ts": resolve_ts, "kronos_prob_up": prob, "kronos_veto": bool(veto), "note": note} append_ledger(row) lines.append(f"[{coin}] rsi15={last_r15:.1f} rsi5={last_r5:.1f} " f"price={price:.2f} -> {action} {note}") print(lines[-1]) if args.push: push_ledger() print("done", now_iso()) if __name__ == "__main__": main()