Building a Pinnacle Arbitrage Bot in Python: From Stream to Signal
Build it end to end: stream live Pinnacle drops, strip the vig, detect a real arbitrage edge, and alert — plus the execution risks that decide if it pays.
Building a Pinnacle Arbitrage Bot in Python: From Stream to Signal
This post connects the dots between three earlier ones — the SSE drop stream, no-vig fair odds, and the arbitrage math — into a single working pipeline. By the end you'll have a bot that listens for live Pinnacle moves, computes a fair reference, checks it against a soft book, and alerts you when there's a real edge. We'll also be honest about the part that separates a demo from a profitable system: execution.
The pipeline is four stages: stream → fair value → edge check → alert. Build them in that order. (If you want the map of which of these layers the feed already runs for you and which are yours, that's its own post — the short answer is stages 1–2 are server-side, 3–4 are your code.)
Prerequisites
- Python 3.10+, with
httpx(async HTTP + SSE-friendly) - A pinnapi key with stream access (the free tier is REST-only; drops need a paid tier)
- A source of soft-book prices to compare against (your own, or another feed)
This is educational. Live betting is restricted in many places, soft books limit and ban arbitrage bettors, and nothing here is advice — it's an architecture.
Stage 1 — Stream the drops
Subscribe to the live drop stream and hand each event to the pipeline. Stamp receive-time first, before any work:
import os, json, time, httpx
KEY = os.environ["PINNAPI_KEY"]
# min_drop=3 → the server only sends drops of 3%+ (default 5, floor 1)
STREAM = f"https://pinnapi.com/odds-drop?key={KEY}&min_drop=3"
async def stream_drops(on_drop):
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream("GET", STREAM) as r:
async for line in r.aiter_lines():
if not line.startswith("data:"):
continue
t_recv = time.time()
payload = json.loads(line[5:].strip())
if isinstance(payload, dict): # {"type": "connected"} handshake / errors
continue
for drop in payload: # each frame carries an array of alerts
await on_drop(drop, t_recv)
Stage 2 — Filter noise, then compute fair value
Most drops aren't tradeable. The min_drop=3 on the subscription already applies a 3% magnitude threshold server-side (the SSE post covers why 3% is a sane starting point); tighten further client-side if your strategy wants it:
MIN_MAG = 0.03 # as a fraction of the pre-drop price
def passes_filter(drop):
mag = (drop["from_price"] - drop["to_price"]) / drop["from_price"]
return mag >= MIN_MAG
For fair value you don't need to compute anything: every alert already carries nvp — the no-vig fair price for the dropped outcome, derived from the fresh post-drop market (here's the math it's doing). The fair probability is just 1 / drop["nvp"].
Using the fresh post-drop price is the whole reason latency matters: a fair value derived from a stale line is a confident wrong answer.
Stage 3 — Check for a real edge
Compare Pinnacle's fair probability to what the soft book offers. Positive EV means the soft book is paying more than fair:
def edge(fair_p, soft_book_odds):
# EV per unit staked; > 0 means the soft book is mispriced in your favor
return fair_p * soft_book_odds - 1
async def on_drop(drop, t_recv):
if not passes_filter(drop):
return
fair_p = 1 / drop["nvp"] # no-vig fair probability
# your soft-book source, keyed by event + outcome + period
soft = get_soft_book_odds(drop["id"], drop["outcome"], drop["period"])
if soft is None:
return
ev = edge(fair_p, soft)
if ev > 0.01: # 1% threshold after a buffer
alert(drop, fair_p, soft, ev, t_recv)
Note the > 0.01 rather than > 0. The buffer absorbs estimation error and the fact that by the time you act, the price has moved a little. A bot that fires on a razor-thin theoretical edge will lose to slippage every time.
Stage 4 — Alert (and why you shouldn't auto-execute on day one)
For a first version, alert — don't auto-bet. Send the signal somewhere you can act on it and, more importantly, log it for later analysis:
def alert(drop, fair_p, soft, ev, t_recv):
latency_ms = t_recv * 1000 - drop["alerted_ms"] # server stamps emission in ms
print(f"[EDGE {ev:+.1%}] {drop['home']} vs {drop['away']} ({drop['outcome']}) "
f"fair_p={fair_p:.3f} soft={soft} latency={latency_ms:.0f}ms")
# also: append to a log for the validation step below
Wiring straight through to placing bets is where people lose money fast. Get the signal right and measured first.
The part that decides everything: execution risk
A backtest that ignores these will lie to you:
- Suspension. Soft books suspend live markets within moments of a notable event. The price you saw may be gone before your bet lands. Plan for the bet to fail, not succeed.
- Partial fills. If one leg places and the other suspends, you're not arbing — you're holding a one-sided directional position you didn't choose.
- Limits and bans. Soft books restrict accounts that consistently arb. This is an operational reality, not an edge case.
- Your own latency. The bot competes with others doing the same thing. Measure your end-to-end latency honestly (methodology here) and assume your competitors are fast too.
Validate before you trust it
Run the alert-only bot for a few hundred signals and log: the EV at alert time, whether the soft price was still there N seconds later, and whether a placed bet would have filled both legs. That last column is the real win rate. If most of your "edges" had vanished or suspended by the time you could act, you've learned something cheap instead of expensive.
Takeaway
The bot itself is straightforward: stream, strip the vig, check the edge, alert. The difficulty — and where the money actually is or isn't — lives in execution: suspension, partial fills, and latency. Build the alert-and-log version first, measure how many signals survive contact with reality, and only then consider automating the part that risks money. For the systems-level view of the same pipeline — the ingestion layer, multi-book normalization, and the honest limits — see how to architect an arbitrage detection system.
Frequently asked questions
Do I need the paid plan?
Yes for the live drop stream; the free tier is REST-only and the stream is what makes this real-time.
Can the bot place bets automatically?
Technically yes, but don't on day one. Alert and log first; auto-execution multiplies both your latency edge and your execution risk.
Why does latency matter so much here?
Fair value is only valid at the instant the line is current, and soft-book windows close fast. Measure your own latency rather than assuming it.
Get real-time Pinnacle odds in your code
Live & prematch markets with sub-second odds-drop alerts. Free trial key in seconds — no card.
Start free trial