Files
Kalshi_Polymarket/server.py
T
Casey Judice a65256ca74 Add Step 1 "Prepare trade" pre-flight ticket (read-only, no execution)
New arb/prepare.py + GET /api/prepare: for one monthly arb pair it re-pulls
fresh quotes (incl. real Kalshi top-of-book ask size) and builds the exact
order pair a cross-venue hedge would need -- sides from best_side, size
capped to the thinner book, limit = ask + slippage buffer, capital required,
guaranteed worst-case, and warnings. Places NO orders and touches no order
endpoints anywhere.

UI: a "Prepare trade" button in the monthly drawer's breakdown card opens a
PREVIEW-ONLY ticket modal (max-size + slippage-cent inputs that re-price live;
a deliberately disabled Confirm button as the Step-2 placeholder). Also
reorders the expanded drawer so the breakdown/Prepare card is the wide left
column.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 22:11:09 -04:00

138 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Prediction-market arb scanner — pure-stdlib HTTP server.
python3 server.py [port] (default 8787)
Routes:
GET / served web UI
GET /api/scan live Kalshi + Polymarket scan (JSON)
GET /api/settings current thresholds
POST /api/settings update thresholds (writes data/settings.json)
"""
import json
import os
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
from arb import scan, prepare
ROOT = os.path.dirname(os.path.abspath(__file__))
WEB = os.path.join(ROOT, "web")
_STATIC = {
"/": ("index.html", "text/html; charset=utf-8"),
"/index.html": ("index.html", "text/html; charset=utf-8"),
"/app.js": ("app.js", "application/javascript; charset=utf-8"),
"/style.css": ("style.css", "text/css; charset=utf-8"),
}
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def _send(self, code, body, ctype="application/json; charset=utf-8"):
if isinstance(body, (dict, list)):
body = json.dumps(body).encode()
elif isinstance(body, str):
body = body.encode()
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def _static(self, path):
name, ctype = _STATIC[path]
try:
with open(os.path.join(WEB, name), "rb") as f:
self._send(200, f.read(), ctype)
except OSError:
self._send(404, {"error": "not found"})
def do_GET(self):
u = urlparse(self.path)
if u.path in _STATIC:
return self._static(u.path)
if u.path in ("/api/scan", "/api/scan/hourly", "/api/scan/daily"):
force = parse_qs(u.query).get("force", ["0"])[0] == "1"
runner = (scan.run_daily if u.path.endswith("daily")
else scan.run_hourly if u.path.endswith("hourly")
else scan.run_scan)
try:
return self._send(200, runner(force=force))
except Exception as e:
return self._send(500, {"error": "%s: %s"
% (type(e).__name__, e)})
if u.path == "/api/positions":
force = parse_qs(u.query).get("force", ["0"])[0] == "1"
try:
return self._send(200, scan.run_positions(force=force))
except Exception as e:
return self._send(500, {"error": "%s: %s"
% (type(e).__name__, e)})
if u.path == "/api/prepare":
# Read-only: build a preview ticket for one pair. Places NO orders.
qs = parse_qs(u.query)
ticker = qs.get("ticker", [""])[0]
slug = qs.get("slug", [""])[0]
raw_max = qs.get("max", [""])[0]
try:
max_size = int(float(raw_max)) if raw_max else None
except (TypeError, ValueError):
max_size = None
try:
buffer_cents = float(qs.get("buffer", ["1"])[0])
except (TypeError, ValueError):
buffer_cents = 1.0
try:
return self._send(200, prepare.run_prepare(
ticker, slug, max_size, buffer_cents))
except Exception as e:
return self._send(500, {"error": "%s: %s"
% (type(e).__name__, e)})
if u.path == "/api/settings":
return self._send(200, scan.load_settings())
self._send(404, {"error": "not found"})
def do_POST(self):
if urlparse(self.path).path != "/api/settings":
return self._send(404, {"error": "not found"})
try:
n = int(self.headers.get("Content-Length") or 0)
incoming = json.loads(self.rfile.read(n) or b"{}")
cur = scan.load_settings()
allowed = ("kalshi_fee_rate", "poly_fee_rate", "min_net_return",
"min_poly_volume", "min_contracts")
for k in allowed:
if k in incoming:
cur[k] = float(incoming[k])
with open(os.path.join(ROOT, "data", "settings.json"), "w") as f:
json.dump(cur, f, indent=2)
scan._CACHE["payload"] = None # force fresh recompute next scan
self._send(200, cur)
except Exception as e:
self._send(400, {"error": "%s: %s" % (type(e).__name__, e)})
def log_message(self, fmt, *args):
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8787
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
print("Arb scanner running -> http://127.0.0.1:%d" % port)
print("Press Ctrl+C to stop.")
from arb import alerts
alerts.start_poller()
try:
srv.serve_forever()
except KeyboardInterrupt:
print("\nStopping.")
srv.shutdown()
if __name__ == "__main__":
main()