2026-05-20 19:48:50 -04:00
|
|
|
#!/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
|
|
|
|
|
|
2026-06-02 22:10:57 -04:00
|
|
|
from arb import scan, prepare
|
2026-05-20 19:48:50 -04:00
|
|
|
|
|
|
|
|
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)
|
2026-05-23 16:16:52 -04:00
|
|
|
if u.path in ("/api/scan", "/api/scan/hourly", "/api/scan/daily"):
|
2026-05-20 19:48:50 -04:00
|
|
|
force = parse_qs(u.query).get("force", ["0"])[0] == "1"
|
2026-05-23 16:16:52 -04:00
|
|
|
runner = (scan.run_daily if u.path.endswith("daily")
|
|
|
|
|
else scan.run_hourly if u.path.endswith("hourly")
|
2026-05-20 19:48:50 -04:00
|
|
|
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)})
|
2026-06-02 22:10:57 -04:00
|
|
|
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)})
|
2026-05-20 19:48:50 -04:00
|
|
|
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
|
2026-06-03 17:55:00 -04:00
|
|
|
if os.environ.get("ARB_NO_POLLER") == "1":
|
|
|
|
|
print("ARB_NO_POLLER=1 -> local SMS poller off (GitHub Actions alerts).")
|
|
|
|
|
else:
|
|
|
|
|
alerts.start_poller()
|
2026-05-20 19:48:50 -04:00
|
|
|
try:
|
|
|
|
|
srv.serve_forever()
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\nStopping.")
|
|
|
|
|
srv.shutdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|