Feed liveChannel BlogSports 12
Transport MQTT / SSE / RESTPush 30–150 msV1
Odds Drops

Odds Drop Detection: What a Real Detector Gets Right

Odds drop detection breaks on suspensions and bad reference windows. How to build a detector that survives real Pinnacle line movement — with field names and code.

Most odds drop detection code is a two-line diff: compare the current price to the last price you saw, and if it fell by more than X percent, fire an alert. That works for about ten minutes of live football, then the suspensions start and your alert channel turns into noise.

The hard part was never the subtraction. It's deciding what you are subtracting from, and knowing when the number you're holding is stale, suspended, or from a different market than you think.

Why drop detection matters at all

A price on Pinnacle falls because money moved it. That's the whole premise. Pinnacle takes sharp action and moves rather than limiting, so a sustained downward move on a selection is closer to information than to noise — and it's the signal a lot of people build around, whether they're following it into other books, timing entries, or just watching for a market that suddenly knows something.

Two words get used loosely here, so let me pin them down:

  • Line movement is any change in price or handicap. Neutral term.
  • Dropping odds means the decimal price on a selection fell (probability up).
  • Steam detection usually means a fast, coordinated move across several books at once. That's a multi-book problem. A Pinnacle-only feed detects the origin of steam, not the steam itself. Be honest with yourself about which one you actually want.

Everything below is about the second and third: catching a real drop on Pinnacle's own prices, fast, without lying to yourself about it.

Where naive detectors fail

Failure 1: suspensions look like drops

Live markets suspend constantly. A corner, a VAR check, a possible goal — the market goes down, and when it comes back the price has repriced hard. If your detector treats "price reappeared at 1.60 after being 2.10" as a drop, you're not detecting money. You're detecting a goal that already happened, announced to you thirty seconds after everyone with the match on screen.

Worse: some feeds represent suspension as a missing market rather than a flag. Your last-known price sits in memory, aging, while reality moves on. Then the market returns and you diff against a corpse.

Rules that actually work:

  • Treat any gap in updates as poisoning the reference, not as a data point. If the market was away longer than your tolerance, drop the reference and re-seed from the reopening price.
  • Never alert on the first tick after a reopen. Ever. Let the market settle for at least one subsequent update.
  • If you have a suspension/status signal, gate on it directly rather than inferring from silence.

Failure 2: the reference window is wrong

"Compared to what?" is the entire design decision. Three common choices, and my opinion on each:

Last tick. Useless on its own. Pinnacle's prices jitter within a tight band; tick-to-tick diffs generate hundreds of 1% "drops" per match.

Fixed lookback (e.g. price 60 seconds ago). Better, but it aliases badly. In a quiet prematch market you get nothing for hours then a jump; in a chaotic live market 60 seconds spans two goals.

Rolling extreme over a recent window — compare the current price against the highest price seen in the last N updates or N seconds. This is the one I'd build. It captures a drop that happened in three small steps instead of one big one, which is exactly how sharp money usually arrives. Small steps are the interesting case; a single 30% lurch is normally a goal.

Whatever you pick, the window must be per selection, per line, not per market. Which brings us to the next failure.

Failure 3: comparing across different lines

Soccer totals live at 1.75, 2.25, 2.75, 3.25. Handicaps at -0.75, -1.25, -1.75, -2.25. When the total shifts from Over 2.5 to Over 2.75, a detector keyed on "Over" alone sees a price change that is really a handicap change. The price on Over 2.5 didn't drop; the market moved the line and your key collapsed two different things into one.

Key your state by (event_id, market, handicap/points, selection). If a line disappears from the payload entirely, expire its state instead of carrying it forward.

Failure 4: no limit awareness

A 12% drop on a market with a tiny max stake is not the same event as a 4% drop on a market taking full limits. Pinnacle publishes the max stake, and ignoring it means you rank a thin corner-market wobble above a genuine main-market move. Sort by conviction, not by percentage alone.

The fields you actually need

pinnodds ships drops two ways, and the shapes differ deliberately. This trips people up, so here it is plainly.

SSE (/odds-drop, /odds-drop-prematch) — the fast path. An alert names the market in sect, the moving selection in outcome, prices in from_price and to_price, the event in id, plus limit (Pinnacle's max stake) and nvp (the no-vig fair price). There is no drop percentage on an SSE alert. You compute it, which is fine because you probably want your own threshold logic anyway.

REST (/api/drops?mode=live|prematch) — the enriched form. Rows carry market, designation, from, to, event_id and a precomputed drop_pct. This is what you poll to backfill state after a restart, or to power a leaderboard view.

Same underlying events, different ergonomics. Use SSE to react and REST to reconcile.

import json, os
from collections import defaultdict, deque
from time import monotonic
from pinnodds import Client  # pip install pinnodds

MAX_GAP = 8.0        # seconds of silence that poisons a reference
WINDOW  = 45.0       # rolling-extreme window
THRESH  = 0.05       # 5% against the window high

state = defaultdict(deque)   # key -> deque[(ts, price)]
last_seen = {}

def key(a):
    return (a["id"], a["sect"], a["outcome"])

def on_alert(a):
    k, now = key(a), monotonic()
    price = float(a["to_price"])

    # Suspension guard: long silence means the reference is untrustworthy.
    gap = now - last_seen.get(k, now)
    last_seen[k] = now
    if gap > MAX_GAP:
        state[k].clear()
        state[k].append((now, price))
        return                      # never alert on the reopening tick

    q = state[k]
    q.append((now, price))
    while q and now - q[0][0] > WINDOW:
        q.popleft()

    high = max(p for _, p in q)
    move = (high - price) / high
    if move >= THRESH and len(q) >= 3:
        print(f"{a['sect']} {a['outcome']}: {high:.3f} -> {price:.3f} "
              f"({move:.1%})  limit={a.get('limit')} nvp={a.get('nvp')}")

for evt in Client(os.environ["PINNODDS_KEY"]).odds_drop_stream():
    on_alert(evt)

Three things that code does which the two-line diff doesn't: it clears the reference after a gap, it compares against a rolling high rather than the previous tick, and it refuses to alert until it has enough samples to know the market is live. That's most of the gap between a demo and something you'd leave running.

If you want the raw price stream instead of pre-filtered drops, /ws/feed is a WebSocket passthrough — every line Pinnacle prices, quarter lines included. Build your own detector on top and you own the whole threshold policy. Field-by-field detail is in the docs.

Push versus poll, and why it decides your ceiling

You cannot detect a fast drop on a slow loop. Poll every 10 seconds and your best-case detection latency is 10 seconds plus request time, and you'll routinely miss the move-and-settle pattern entirely because you sample the "after" price and never see the "before".

pinnodds pushes. Odds arrive over WebSocket in real time, and drop alerts arrive over SSE as they happen — sub-second, no polling loop. REST is there for state you need on demand: /kit/v1/markets (live or prematch via event_type), /kit/v1/details, /kit/v1/prematch/fixtures, /kit/v1/prematch/markets, /kit/v1/prematch/lines. Health checks on /health and /ping. One API key, sent as x-api-key, or ?key= on the streams.

Prematch deserves a note: drops there are slower but often cleaner, because there's no in-play chaos manufacturing false positives. /odds-drop-prematch and mode=prematch cover it. If you're starting out, start prematch — the signal-to-noise is friendlier and your detector's bugs are easier to see.

Specials, and the payload cliff

Player props, exact scores, futures and outrights are available with include_specials=1. They arrive as their own event rows carrying special, special_category, special_markets and a parent_id back to the parent fixture.

They're off by default for a good reason. Soccer prematch is roughly 1,500 events without the flag and roughly 12,400 with it. Props also move in bigger, jumpier steps than main markets, so a threshold tuned on 1X2 will spam you on player shots-on-target. Tune specials separately or don't run them through the same detector.

Where this feed is the wrong tool

Say it plainly:

  • It's Pinnacle only. If your definition of steam detection requires seeing 15 books move within the same second, this is one input, not the answer. Pair it with an aggregator.
  • No historical odds archive. You get real time. If you want to backtest a drop threshold over last season, you have to record the feed yourself starting now. That's a real cost and it's worth planning for on day one — start logging before you start tuning.
  • SSE alerts carry no drop_pct. If you want the precomputed number, that's the REST /api/drops shape. Deciding this in advance saves a rewrite.
  • Push access isn't on every plan. Depth is identical everywhere — every quarter line, every plan — but plans differ in rate limit and push access, so a polling-only plan caps your detection latency by design. Details at pricing.
  • A drop is not a bet. Pinnacle moving means Pinnacle got hit. Whether that edge still exists at the book you can actually get on is your problem, not the feed's.

Takeaway

Build your detector state keyed by (event_id, market, line, selection), compare against a rolling high over a short window rather than the previous tick, and treat every gap in updates as a hard reset that suppresses the next alert. Those three rules kill the overwhelming majority of false positives in odds drop detection, and none of them require clever maths — just refusing to trust a price you haven't seen confirmed. Weight what survives by limit and sanity-check against nvp, and you have a ranking that reflects conviction instead of arithmetic. Grab a trial key, point it at /odds-drop-prematch for a day, and log everything before you tune anything.

Frequently asked questions

What is odds drop detection?

Odds drop detection is spotting when a selection's price falls against its own recent history — typically a sign that money has moved the market. On Pinnacle specifically it's treated as a sharp-money signal, because Pinnacle moves prices rather than limiting winning accounts.

How do you avoid false odds drop alerts from suspended markets?

Treat any gap in updates as poisoning your reference price: clear the stored history, re-seed from the reopening price, and never alert on the first tick after a market returns. A goal-driven reprice looks identical to a drop if you diff against a pre-suspension price.

Is odds drop detection the same as steam detection?

No. Steam detection means a fast, coordinated move across many bookmakers at once, which needs a multi-book feed. A Pinnacle-only feed detects the origin of that move, which is often earlier but is a single input, not a full steam picture.

Does the pinnodds SSE feed include a drop percentage?

No. SSE alerts on /odds-drop and /odds-drop-prematch carry sect, outcome, from_price, to_price, id, limit and nvp — you compute the percentage yourself. The REST endpoint /api/drops returns the enriched form with a precomputed drop_pct.

Can I backtest an odds drop strategy with pinnodds?

Not from the API — there is no historical odds archive; the service is real-time. If you want to backtest thresholds you need to record the WebSocket or SSE stream yourself, so start logging before you start tuning.

Is there still a public Pinnacle API?

Pinnacle closed its own public API in July 2025. pinnodds is an independent service that carries the same market data over WebSocket, SSE and REST, authenticated with a single x-api-key header.

Get real-time Pinnacle odds in your code

The Pinnacle APIby pinnodds: live & prematch markets with instant odds-drop alerts over SSE and WebSocket. See plans and pricing, or get a free key in seconds — no card, no Pinnacle account.

Get your free API key