kronos-data / quad_autopilot.py
40Hz's picture
autoresearch v2: honest grid
e99d55c verified
Raw History Blame Contribute Delete
8.59 kB
"""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()