HomeBlog › Building a Pinnacle Arbitrage Bot in Python: From Stream to Signal
Guides

Building a Pinnacle Arbitrage Bot in Python: From Stream to Signal

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

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:

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