Files
Michelle 4aa8789cba Initial commit — BTC 5-minute binary options edge study
Reconstructed strategy engine + execution layer, trained XGBoost models, a manual
trading tool, and the research writeup. Paper mode runs keyless over live WebSocket
feeds; live trading requires your own wallet. No secrets committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 22:49:05 -07:00

666 lines
28 KiB
Python

#!/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())