#!/usr/bin/env python3 """Quick trade — press U/D to instantly buy. Watch the chainlink_watch for prices. !! LIVE, REAL MONEY. Every keystroke places a REAL order on Polymarket with the wallet in src/predictor/.env. There is NO paper mode and NO confirmation dialog. Provided as-is and UNAUDITED — read the code first. Needs the collector (tools/chainlink_predictor.py) running for live prices. Use at your own risk. Keys: u → Buy UP 10sh d → Buy DOWN 10sh (market ask + 0.02) ! → Buy UP @0.01 $ → Buy DOWN @0.01 (Shift+1/4, limit order) @ → Buy UP @0.02 % → Buy DOWN @0.02 (Shift+2/5) # → Buy UP @0.03 ^ → Buy DOWN @0.03 (Shift+3/6) i → Sell UP o → Sell DOWN (sells most expensive first) b → Balance r → Redeem w → Refresh window q → Quit """ from __future__ import annotations import asyncio import json import math import os import sys import time import tty import termios import subprocess import select sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src', 'predictor', '.env'), override=True) import aiohttp from web3 import Web3 from py_clob_client_v2.client import ClobClient from py_clob_client_v2.clob_types import OrderArgs, OrderType, PartialCreateOrderOptions from py_clob_client_v2.order_builder.constants import BUY import httpx import py_clob_client_v2.http_helpers.helpers as _clob_helpers G = "\033[92m" R = "\033[91m" B = "\033[1m" C = "\033[96m" Y = "\033[93m" DIM = "\033[2m" RST = "\033[0m" BUCKET_SEC = 300 TRADE_SHARES = 10 # fixed 10 shares per click MAX_ASK = 0.99 SNAPSHOT_FILE = "data/chainlink_predictor/snapshots.jsonl" MANUAL_TRADES_FILE = "data/chainlink_predictor/manual_trades.jsonl" class QuickTrader: def __init__(self): _clob_helpers._http_client = httpx.Client(http2=False, timeout=30) key = os.environ.get("PREDICTOR_WALLET_KEY", "") address = os.environ.get("PREDICTOR_WALLET_ADDRESS", "") self.address = address self.client = ClobClient("https://clob.polymarket.com", key=key, chain_id=137, signature_type=0, funder=address) self.client.set_api_creds(self.client.create_or_derive_api_key()) self.token_up = "" self.token_down = "" self.window_id = 0 self._preflight_cache: dict[str, tuple] = {} self.pending_trades: list[dict] = [] # Tail snapshots for live prices self._snap_proc = None self._last_clob = {} self._last_left = 0.0 self._last_pm_a = 0.0 self._last_sim_a = 0.0 if os.path.exists(SNAPSHOT_FILE): self._snap_proc = subprocess.Popen( ["tail", "-F", "-n", "1", SNAPSHOT_FILE], stdout=subprocess.PIPE, text=True, ) def read_snapshot(self): if not self._snap_proc: return while True: ready, _, _ = select.select([self._snap_proc.stdout], [], [], 0) if not ready: break line = self._snap_proc.stdout.readline() if not line: break try: d = json.loads(line) self._last_clob = d.get("clob", {}) self._last_left = d.get("left_sec", 0) self._last_pm_a = d.get("pm_a", 0) sim = d.get("sim", 0) pm = d.get("pm", 0) pm_a = d.get("pm_a", 0) ptb = pm - pm_a if pm_a else pm self._last_sim_a = sim - ptb if sim > 0 and ptb > 0 else 0 except Exception: pass async def discover(self): now = time.time() base = int(now // BUCKET_SEC) * BUCKET_SEC async with aiohttp.ClientSession() as session: for ts in [base, base + BUCKET_SEC, base - BUCKET_SEC]: slug = f"btc-updown-5m-{ts}" try: async with session.get("https://gamma-api.polymarket.com/events", params={"slug": slug}, timeout=aiohttp.ClientTimeout(total=5)) as resp: if resp.status != 200: continue data = await resp.json() if not data: continue market = data[0].get("markets", [{}])[0] raw_ids = market.get("clobTokenIds") token_ids = json.loads(raw_ids) if isinstance(raw_ids, str) else raw_ids if token_ids and len(token_ids) >= 2: self.token_up = token_ids[0] self.token_down = token_ids[1] self.window_id = ts await self._warm(self.token_up) await self._warm(self.token_down) return True except Exception: continue return False async def _warm(self, token_id: str): if token_id in self._preflight_cache: return try: tick = await asyncio.to_thread(self.client.get_tick_size, token_id) neg = await asyncio.to_thread(self.client.get_neg_risk, token_id) self._preflight_cache[token_id] = (tick, neg) except Exception: pass async def buy(self, direction: str) -> str: token_id = self.token_up if direction == "UP" else self.token_down if not token_id: return f"{R}No token — press W to refresh{RST}" self.read_snapshot() ua = self._last_clob.get("up_ask", 0) da = self._last_clob.get("down_ask", 0) if direction == "UP": price = ua else: price = da if price <= 0: other = da if direction == "UP" else ua if other > 0: price = round(1.0 - other, 2) if price <= 0: return f"{R}No price{RST}" if price > MAX_ASK: return f"{R}Too expensive: {price:.2f}{RST}" # Add $0.02 buffer for fill, cap at MAX_ASK limit = min(round(price, 2), MAX_ASK) qty = TRADE_SHARES # Auto-adjust qty to meet $1 minimum if qty * limit < 1.0: qty = math.ceil(1.0 / limit) cost = round(qty * limit, 2) try: preflight = self._preflight_cache.get(token_id) if not preflight: await self._warm(token_id) preflight = self._preflight_cache.get(token_id) tick_size, neg_risk = preflight left = (self.window_id + BUCKET_SEC) - time.time() exp = int(time.time()) + 90 # GTD requires at least now+60s order_args = OrderArgs(price=limit, size=float(qty), side=BUY, token_id=token_id, expiration=str(exp)) options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk) t0 = time.monotonic() signed = await asyncio.to_thread(self.client.create_order, order_args, options) result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD) latency = (time.monotonic() - t0) * 1000 oid_full = result.get("orderID", "") trade = { "ts": int(time.time()), "window_id": self.window_id, "order_id": oid_full, "direction": direction, "qty": qty, "price": limit, "display_price": price, "cost": round(qty * limit, 2), "left_sec": round(left, 1), "pm_a": self._last_pm_a if hasattr(self, '_last_pm_a') else 0, "settled": False, "won": False, "pnl": 0, } self.pending_trades.append(trade) with open(MANUAL_TRADES_FILE, "a") as f: f.write(json.dumps(trade) + "\n") color = G if direction == "UP" else R return f"{color}{B}{direction}{RST} {qty}@{price:.2f} (limit {limit:.2f}) = ${qty*limit:.2f} | {latency:.0f}ms wid={self.window_id}" except Exception as e: return f"{R}Failed: {e}{RST}" async def buy_fixed(self, direction: str, price: float, qty: int) -> str: """Buy at a fixed price and quantity (for cheap lottery tickets).""" token_id = self.token_up if direction == "UP" else self.token_down if not token_id: return f"{R}No token — press W to refresh{RST}" # Auto-adjust qty to meet $1 minimum if qty * price < 1.0: qty = math.ceil(1.0 / price) cost = round(qty * price, 2) try: preflight = self._preflight_cache.get(token_id) if not preflight: await self._warm(token_id) preflight = self._preflight_cache.get(token_id) tick_size, neg_risk = preflight left = (self.window_id + BUCKET_SEC) - time.time() # GTD with minimum 90s expiration (API requires at least 60s) # Won't actually last — window settles and tokens become worthless exp = int(time.time()) + 90 order_args = OrderArgs(price=price, size=float(qty), side=BUY, token_id=token_id, expiration=str(exp)) options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk) t0 = time.monotonic() signed = await asyncio.to_thread(self.client.create_order, order_args, options) result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD) latency = (time.monotonic() - t0) * 1000 oid_full = result.get("orderID", "") trade = { "ts": int(time.time()), "window_id": self.window_id, "order_id": oid_full, "direction": direction, "qty": qty, "price": price, "display_price": price, "cost": cost, "left_sec": round(left, 1), "pm_a": self._last_pm_a if hasattr(self, '_last_pm_a') else 0, "settled": False, "won": False, "pnl": 0, } self.pending_trades.append(trade) with open(MANUAL_TRADES_FILE, "a") as f: f.write(json.dumps(trade) + "\n") color = G if direction == "UP" else R return f"{color}{B}{direction}{RST} {qty}@{price:.2f} = ${cost:.2f} | {latency:.0f}ms wid={self.window_id}" except Exception as e: return f"{R}Failed: {e}{RST}" async def sell(self, sell_dir: str = "") -> str: """Sell tokens. sell_dir='UP'/'DOWN' to choose, or '' to sell most recent.""" trade = None if sell_dir: # Find matching trade — most recent first matching = [t for t in self.pending_trades if t["direction"] == sell_dir] if matching: trade = matching[-1] else: # No pending trade, but check chain balance directly (e.g. from cb_lead_live) direction = sell_dir token_id = self.token_up if direction == "UP" else self.token_down if token_id: trade = {"direction": direction, "price": 0, "ts": int(time.time()), "_chain_only": True} elif self.pending_trades: trade = self.pending_trades[-1] if not trade: return f"{R}No position to sell{RST}" direction = trade["direction"] token_id = self.token_up if direction == "UP" else self.token_down if not token_id: return f"{R}No token{RST}" # Check on-chain balance for this token try: from web3 import Web3 as _W3 rpc = os.environ.get("POLYGON_RPC_URL", "") w3 = _W3(_W3.HTTPProvider(rpc)) eoa = _W3.to_checksum_address(self.address) CTF = _W3.to_checksum_address("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045") ctf = w3.eth.contract(address=CTF, abi=[ {"name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "account", "type": "address"}, {"name": "id", "type": "uint256"}], "outputs": [{"type": "uint256"}]}]) bal = ctf.functions.balanceOf(eoa, int(token_id)).call() shares = bal / 1e6 except Exception as e: return f"{R}Balance check failed: {e}{RST}" if shares < 1: return f"{R}No shares to sell (balance={shares:.2f}){RST}" # Get best bid try: book = await asyncio.to_thread(self.client.get_order_book, token_id) # V2: get_order_book returns dict (was OrderBookSummary in V1) bids = (book.get("bids") or []) if isinstance(book, dict) else (getattr(book, "bids", None) or []) if not bids: return f"{R}No bids available{RST}" first = bids[0] best_bid = float(first["price"]) if isinstance(first, dict) else float(first.price) except Exception as e: return f"{R}Order book error: {e}{RST}" if best_bid <= 0: return f"{R}No bid price{RST}" # Place SELL order try: from py_clob_client_v2.order_builder.constants import SELL preflight = self._preflight_cache.get(token_id) if not preflight: await self._warm(token_id) preflight = self._preflight_cache.get(token_id) tick_size, neg_risk = preflight sell_qty = math.floor(shares * 100) / 100 # truncate to 2dp exp = int(time.time()) + 90 order_args = OrderArgs(price=best_bid, size=sell_qty, side=SELL, token_id=token_id, expiration=str(exp)) options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk) t0 = time.monotonic() signed = await asyncio.to_thread(self.client.create_order, order_args, options) result = await asyncio.to_thread(self.client.post_order, signed, OrderType.GTD) latency = (time.monotonic() - t0) * 1000 order_id = result.get("orderID", "") # API confirm — get actual fill price from trades history import asyncio as _aio await _aio.sleep(2) matched = 0 status = "" actual_price = best_bid try: raw_client = getattr(self.client, '_client', None) or self.client order_info = await asyncio.to_thread(raw_client.get_order, order_id) if order_info: matched = float(order_info.get("size_matched", 0)) status = order_info.get("status", "") # get_order.price is the LIMIT price, not fill price # Use get_trades to find actual fill price from py_clob_client_v2.clob_types import TradeParams recent_trades = await asyncio.to_thread( raw_client.get_trades, TradeParams(asset_id=token_id, after=int(time.time()) - 30) ) if recent_trades: for tr in recent_trades: if abs(float(tr.get("size", 0)) - matched) < 1: actual_price = float(tr.get("price", best_bid)) break except Exception: pass if matched <= 0: return f"{R}SELL NOT FILLED{RST} {direction} {sell_qty}@{best_bid:.2f} status={status} | {latency:.0f}ms" sell_value = matched * actual_price buy_price = trade.get("price", 0) buy_cost = trade.get("cost", matched * buy_price) pnl = sell_value - buy_cost sell_record = { "ts": int(time.time()), "window_id": trade["window_id"], "order_id": order_id, "action": "SELL", "direction": direction, "qty": matched, "sell_price": actual_price, "limit_price": best_bid, "buy_price": buy_price, "pnl": round(pnl, 2), "api_status": status, "api_matched": matched, } with open(MANUAL_TRADES_FILE, "a") as f: f.write(json.dumps(sell_record) + "\n") if trade in self.pending_trades: self.pending_trades.remove(trade) color = G if pnl >= 0 else R return ( f"{G}✓ SOLD{RST} {direction} {matched:.0f}@{actual_price:.2f} " f"(bought @{buy_price:.2f}) " f"pnl={color}${pnl:+.2f}{RST} | {latency:.0f}ms" ) except Exception as e: return f"{R}Sell failed: {e}{RST}" async def check_results(self): import requests settled = [] now = time.time() for trade in list(self.pending_trades): # Step 1: verify fill via API (once, a few seconds after order) order_id = trade.get("order_id", "") if order_id and not trade.get("fill_verified") and now - trade.get("ts", 0) > 3: try: raw_client = getattr(self.client, '_client', None) or self.client order = await asyncio.to_thread(raw_client.get_order, order_id) if order: matched = float(order.get("size_matched", 0)) status = order.get("status", "") trade["fill_verified"] = True trade["order_status"] = status trade["matched"] = matched if matched > 0: trade["filled"] = True trade["fill_qty"] = matched settled.append(f"{G}✓ FILLED{RST} {trade['direction']} {matched:.0f}@{trade['price']:.2f} (status={status})") elif status in ("EXPIRED", "CANCELLED"): trade["filled"] = False settled.append(f"{R}✗ NOT FILLED{RST} {trade['direction']} @{trade['price']:.2f} — {status}") self.pending_trades.remove(trade) # else LIVE/MATCHED — check again later except Exception: pass # Step 2: settle after window ends (window_id + 300s + 30s grace) window_end = trade["window_id"] + BUCKET_SEC + 30 if now < window_end: continue # Skip settlement if we know it didn't fill if trade.get("fill_verified") and not trade.get("filled"): continue slug = f"btc-updown-5m-{trade['window_id']}" try: r = requests.get("https://gamma-api.polymarket.com/events", params={"slug": slug}, timeout=5) data = r.json() if not data: continue market = data[0].get("markets", [{}])[0] prices = market.get("outcomePrices", "") if isinstance(prices, str) and prices: prices = json.loads(prices) if prices and len(prices) >= 2: up_p = float(prices[0]) # Only settle when price is definitively 0 or 1 is_settled = up_p >= 0.99 or up_p <= 0.01 if is_settled: actual = "UP" if up_p > 0.5 else "DOWN" won = trade["direction"] == actual fill_qty = trade.get("fill_qty", trade["qty"]) fill_price = trade.get("price", 0) pnl = fill_qty * (1.0 - fill_price) if won else -fill_qty * fill_price color = G if won else R w = "WIN" if won else "LOSS" filled_str = f" (filled {fill_qty:.0f})" if trade.get("fill_verified") else "" settled.append(f"{color}{B}{w}{RST} {trade['direction']} {fill_qty:.0f}@{fill_price:.2f} pnl={color}${pnl:+.2f}{RST}{filled_str}") trade["settled"] = True trade["won"] = won trade["pnl"] = round(pnl, 2) trade["actual"] = actual with open(MANUAL_TRADES_FILE, "a") as f: f.write(json.dumps(trade) + "\n") self.pending_trades.remove(trade) except Exception: pass return settled async def get_balance(self) -> float: rpc = os.environ.get("POLYGON_RPC_URL", "") w3 = Web3(Web3.HTTPProvider(rpc)) usdc = w3.eth.contract( address=Web3.to_checksum_address("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"), abi=[{"name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]}], ) bal = usdc.functions.balanceOf(Web3.to_checksum_address(self.address)).call() return bal / 1e6 def cleanup(self): if self._snap_proc: self._snap_proc.terminate() async def main(): trader = QuickTrader() print(f"{B}Quick Trade{RST} — initializing...") await trader.discover() bal = await trader.get_balance() print(f"{B}Quick Trade{RST} — READY Balance: {G}${bal:.2f}{RST}") print(f" {G}U{RST}=Buy UP 5 shares {R}D{RST}=Buy DOWN 5 shares (press multiple times to add)") print(f" {C}B{RST}=Balance {C}R{RST}=Redeem {C}Q{RST}=Quit\n") fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) _window_count = 0 # count windows for auto-redeem _last_redeem_wid = 0 try: tty.setcbreak(fd) while True: # Read snapshots in background trader.read_snapshot() # Auto-refresh window left = (trader.window_id + BUCKET_SEC) - time.time() if left < -5: await trader.discover() _window_count += 1 print(f" {DIM}[new window wid={trader.window_id}]{RST}") # Auto-redeem disabled — conflicts with manual trading nonce # Press R to redeem manually if False and _window_count % 3 == 0 and trader.window_id != _last_redeem_wid: _last_redeem_wid = trader.window_id try: from src.predictor.executor import PredictorExecutor executor = PredictorExecutor() now = int(time.time()) base = now - (now % BUCKET_SEC) total = 0 for i in range(20): wid = str(base - i * BUCKET_SEC) delta = await executor.auto_redeem(f"btc-updown-5m-{wid}") if delta > 0: total += delta # Also redeem pending manual trades for t in trader.pending_trades: wid = str(t["window_id"]) delta = await executor.auto_redeem(f"btc-updown-5m-{wid}") if delta > 0: total += delta if total > 0: print(f" {G}[auto-redeem +${total:.2f}]{RST}") except Exception as e: pass # silent fail # Check results if trader.pending_trades: results = await trader.check_results() for r in results: print(f" {r}") # Non-blocking key check ready, _, _ = select.select([sys.stdin], [], [], 0.5) if not ready: continue key = sys.stdin.read(1) if key in ("q", "Q", "\x03"): print("\n Bye.") break elif key in ("u", "U"): result = await trader.buy("UP") print(f" {result}") elif key in ("d", "D"): result = await trader.buy("DOWN") print(f" {result}") elif key in ("b", "B"): bal = await trader.get_balance() print(f" Balance: {G}${bal:.2f}{RST}") elif key == "!": result = await trader.buy_fixed("UP", 0.01, 100) print(f" {result}") elif key == "$": result = await trader.buy_fixed("DOWN", 0.01, 100) print(f" {result}") elif key == "@": result = await trader.buy_fixed("UP", 0.02, 50) print(f" {result}") elif key == "%": result = await trader.buy_fixed("DOWN", 0.02, 50) print(f" {result}") elif key == "#": result = await trader.buy_fixed("UP", 0.03, 35) print(f" {result}") elif key == "^": result = await trader.buy_fixed("DOWN", 0.03, 35) print(f" {result}") elif key == "i": result = await trader.sell("UP") print(f" {result}") elif key == "o": result = await trader.sell("DOWN") print(f" {result}") elif key in ("w", "W"): await trader.discover() print(f" Window refreshed: {trader.window_id}") elif key in ("r", "R"): print(f" Redeeming...") from src.predictor.executor import PredictorExecutor executor = PredictorExecutor() # Redeem from pending trades + recent windows seen = set() # 1. Pending manual trades for t in trader.pending_trades: seen.add(str(t["window_id"])) # 2. Recent windows (last 2 hours) now = int(time.time()) base = now - (now % BUCKET_SEC) for i in range(24): # last 24 windows = 2 hours seen.add(str(base - i * BUCKET_SEC)) # 3. From predictor trades file trades_file = "data/chainlink_predictor/predictor_trades.jsonl" if os.path.exists(trades_file): with open(trades_file) as f: for line in f: try: d = json.loads(line) wid = d.get("window_id", "") if wid: seen.add(wid) except Exception: pass total = 0 for wid in seen: delta = await executor.auto_redeem(f"btc-updown-5m-{wid}") if delta > 0: print(f" {G}+${delta:.2f}{RST}") total += delta if total > 0: print(f" Total: {G}+${total:.2f}{RST}") bal = await trader.get_balance() print(f" Balance: {G}${bal:.2f}{RST}") finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) trader.cleanup() if __name__ == "__main__": asyncio.run(main())