mirror of
https://github.com/cjudice-commits/prediction-market-arb.git
synced 2026-07-27 21:47:46 +00:00
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>
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
"""Build a read-only 'what would be sent' trade ticket for ONE arb pair.
|
||||
|
||||
*** THIS MODULE PLACES NO ORDERS. ***
|
||||
|
||||
It only reads live quotes (the same public / read-only calls the scanner
|
||||
already makes) and computes the exact pair of orders a cross-venue hedge would
|
||||
require — side, size, limit price, capital, and guaranteed worst-case — so the
|
||||
user can preview a trade before any execution path is ever wired up.
|
||||
|
||||
No Kalshi/Polymarket *order* endpoints are touched anywhere in this file.
|
||||
"""
|
||||
import time
|
||||
|
||||
from . import calc, kalshi, poly, scan
|
||||
|
||||
|
||||
def _int(x):
|
||||
return int(x) if x is not None else None
|
||||
|
||||
|
||||
def run_prepare(ticker, slug, max_size=None, buffer_cents=1.0):
|
||||
"""Return a preview ticket dict for the pair (ticker, slug). Read-only."""
|
||||
if not ticker or not slug:
|
||||
return {"error": "ticker and slug are required"}
|
||||
|
||||
pair = next((p for p in scan.load_pairs()
|
||||
if p.get("kalshi_ticker") == ticker
|
||||
and p.get("poly_slug") == slug), None)
|
||||
if not pair:
|
||||
return {"error": "pair not found in pairs.json"}
|
||||
settings = scan.load_settings()
|
||||
|
||||
# Fresh, read-only quotes for just this one pair.
|
||||
kq = kalshi.fetch_quotes([ticker]).get(ticker)
|
||||
pq = poly.fetch_quotes([slug]).get(slug)
|
||||
# Real Kalshi top-of-book ask size (the scan uses open interest as a proxy
|
||||
# off-candidate; for a trade ticket we want the executable size).
|
||||
szs = (kalshi.fetch_sizes([ticker]) or {}).get(ticker)
|
||||
if szs and kq:
|
||||
if szs.get("yes_ask_size") is not None:
|
||||
kq["yes_ask_size"] = szs["yes_ask_size"]
|
||||
if szs.get("no_ask_size") is not None:
|
||||
kq["no_ask_size"] = szs["no_ask_size"]
|
||||
|
||||
row = calc.evaluate(pair, kq, pq, settings)
|
||||
if not row.get("best_side"):
|
||||
return {"error": "no executable hedge right now",
|
||||
"status": row.get("status"), "asset": row.get("asset")}
|
||||
|
||||
buf = (buffer_cents or 0.0) / 100.0
|
||||
|
||||
# best_side "YES+NO" = Kalshi YES + Poly NO ; "NO+YES" = Kalshi NO + Poly YES
|
||||
k_side, p_side = (("yes", "no") if row["best_side"] == "YES+NO"
|
||||
else ("no", "yes"))
|
||||
k_ask, p_ask = row.get("kalshi_price"), row.get("poly_price")
|
||||
# Marketable limit = ask + buffer, capped below $1. Kalshi ticks in cents,
|
||||
# Polymarket finer — round each to its grid.
|
||||
k_limit = min(0.99, round(k_ask + buf, 2)) if k_ask is not None else None
|
||||
p_limit = min(0.999, round(p_ask + buf, 3)) if p_ask is not None else None
|
||||
|
||||
avail = row.get("max_contracts")
|
||||
size, capped = avail, None
|
||||
if max_size and avail is not None and max_size < avail:
|
||||
size, capped = max_size, "your max"
|
||||
size = _int(size)
|
||||
|
||||
ksz, psz = row.get("kalshi_size"), row.get("poly_size")
|
||||
thin = None
|
||||
if ksz is not None and psz is not None:
|
||||
thin = "Kalshi" if ksz <= psz else "Polymarket"
|
||||
|
||||
k_cost = size * k_ask if (size and k_ask is not None) else None
|
||||
p_cost = size * p_ask if (size and p_ask is not None) else None
|
||||
capital = (k_cost or 0) + (p_cost or 0) if size else None
|
||||
|
||||
w_ct = row.get("worst_pnl")
|
||||
w_total = w_ct * size if (w_ct is not None and size) else None
|
||||
fee_total = (row.get("total_fee") or 0) * size if size else None
|
||||
|
||||
warnings = []
|
||||
if not size:
|
||||
warnings.append("No executable size on at least one leg right now.")
|
||||
elif capped:
|
||||
warnings.append("Size capped to %s (%d contracts)." % (capped, size))
|
||||
elif thin:
|
||||
warnings.append(
|
||||
"Size capped to the thinner %s book (%d contracts)." % (thin, size))
|
||||
if w_ct is not None and w_ct < 0:
|
||||
warnings.append(
|
||||
"Worst case is NEGATIVE — this is not a guaranteed-positive arb.")
|
||||
if row.get("status") != "ARB":
|
||||
warnings.append("Scanner status is %s, not ARB." % row.get("status"))
|
||||
|
||||
return {
|
||||
"preview_only": True, # never an executable instruction
|
||||
"asset": row.get("asset"),
|
||||
"status": row.get("status"),
|
||||
"as_of": time.strftime("%H:%M:%S"),
|
||||
"size": size,
|
||||
"available": _int(avail),
|
||||
"thin_leg": thin,
|
||||
"guaranteed_positive": (w_ct is not None and w_ct >= 0),
|
||||
"buffer_cents": buffer_cents,
|
||||
"kalshi": {
|
||||
"venue": "Kalshi", "ticker": ticker, "action": "buy",
|
||||
"side": k_side, "ask": k_ask, "limit": k_limit,
|
||||
"size": size, "cost": k_cost, "title": row.get("kalshi_title"),
|
||||
},
|
||||
"poly": {
|
||||
"venue": "Polymarket", "slug": slug, "action": "buy",
|
||||
"side": p_side, "ask": p_ask, "limit": p_limit,
|
||||
"size": size, "cost": p_cost, "question": row.get("poly_question"),
|
||||
},
|
||||
"combined_cost_ct": row.get("combined_cost"),
|
||||
"capital_required": capital,
|
||||
"worst_pnl_ct": w_ct,
|
||||
"worst_pnl_total": w_total,
|
||||
"best_pnl_ct": row.get("best_pnl"),
|
||||
"fees_total": fee_total,
|
||||
"net_return": row.get("net_return"),
|
||||
"warnings": warnings,
|
||||
}
|
||||
Reference in New Issue
Block a user