mirror of
https://github.com/cjudice-commits/prediction-market-arb.git
synced 2026-07-27 21:47:46 +00:00
Initial commit: prediction-market arb scanner + GHA alerts
- Monthly + hourly Kalshi/Polymarket arb scanner (stdlib-only Python). - Live positions tab w/ realized P&L history. - GitHub Actions cron workflow texts SMS via Apps Script webhook on newly-detected arbs. State persisted in alerts_state.json. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/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
|
||||
|
||||
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"):
|
||||
force = parse_qs(u.query).get("force", ["0"])[0] == "1"
|
||||
runner = (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/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()
|
||||
Reference in New Issue
Block a user