Feed liveChannel BlogSports 12
Transport MQTT / SSE / RESTPush 100–500 msV1
Guides

Map Asian Handicap Lines Between Bookmakers

How to map Asian handicap lines between bookmakers without sign-flip bugs: anchor every line to the home team, scope by period, and derive the away side on read.

Pick one anchor team — the home side — and store every handicap as the number applied to that team. Do that and most of the bugs people hit when they try to map Asian handicap lines between bookmakers simply stop existing, because the sign flip is the bug. The rest of this post is detail hanging off that one rule.

Sign errors don't fail loudly. They produce a confidently wrong signal: you think you've found +EV on the away team at -0.75 when the two feeds are quoting opposite ends of the same market. The number looks plausible. Nothing throws. You bet it.

The short definition

An Asian handicap is a goal (or point, or run) adjustment applied to one team before settlement, quoted as a signed number relative to a named side. "Home -1.25" and "Away +1.25" are the same market state described from opposite ends. A feed that hands you handicap: -1.25 without unambiguously telling you whose handicap it is has given you half a data point, and half a data point is worse than none because it looks complete.

The normalised row you want in your database:

(event_id, market_type, period, home_line, home_price, away_price)

Note what is not there: an away line. Don't store it. It is always -home_line, derived on read. The moment you persist both sides independently you've built a mechanism for them to disagree, and eventually they will — a partial update, a retried write, two workers racing on the same event.

Totals are the easy sibling. The line is shared (Over 2.75 / Under 2.75), so you store one number and two prices and there is nothing to anchor. Handicaps are awkward precisely because the number is side-relative.

What it gets confused with: European handicaps and point spreads

Three things sit next to each other in most schemas and get muddled.

Asian handicap. Two outcomes, no draw. Quarter lines split the stake across two adjacent half lines — which is the entire reason -0.75 exists: half at -0.5, half at -1.0.

European handicap. Three outcomes — home, draw, away — with a whole-number handicap. "Home -1, draw" is a settleable result in EH and does not exist in AH. Shove EH rows into an AH table because both objects happen to have a field called handicap and your model will price a two-way market off three-way data, silently, forever.

North American point spreads. Asian handicaps wearing a different hat, usually hooked (-3.5) to avoid pushes and denominated in points. Same normalisation rules, different vocabulary. Handicap sign convention differences between a soccer feed and a basketball feed are almost always presentational rather than structural.

Rule I'd enforce at the schema level: key storage on an explicit market_type (ah, eh, total, ml) and never infer it from the mere presence of a handicap value.

Why the anchor matters more than it sounds

Half a goal is not a rounding difference. The step from -0.5 to -0.75 changes the settlement of every one-goal win, and the fair-price gap between adjacent quarter lines is large enough that getting it wrong eats whatever edge you thought you'd found.

Now stack a sign error on top. Read Pinnacle's home -0.75 and a soft book's away -0.75 as the same row and you're staring at a two-line-wide gap — roughly 1.5 goals of implied difference — and reading it as a mispricing worth hammering. It isn't a mispricing. It's your ETL.

That's the shape of every serious bug in this area: both numbers are valid floats, both markets genuinely exist, and only the pairing is wrong.

The honest caveats

Things that clean code will not save you from.

Fixture matching is harder than line matching, by a lot. Before you can compare a handicap you have to be certain both feeds mean the same match. Team names are a swamp: "Man Utd" / "Manchester United" / "Manchester Utd FC", accents, transliterations, and reserve or youth sides sharing a name with the senior team. Match on kickoff timestamp within a tolerance (±15 minutes is a sane starting point, wider for feeds with sloppy scheduling) and a normalised name, then keep an alias table for the residue. Nothing gets you to 100%. Budget maintenance time for this, not for the sign arithmetic.

Period scoping is a silent killer. Full-time, first-half and second-half-only handicaps all carry a handicap field. Some feeds default to full-time and flag the exceptions; others emit periods as separate market rows. Compare a 1H -0.25 against an FT -0.25 and you will see "value" on literally every match, which is the tell.

pinnodds is Pinnacle, not the market. This matters most on this exact topic. We serve real-time live and prematch odds sourced from Pinnacle — one book, deeply. If your strategy is a 40-book comparison grid, we are one leg of it and you need an aggregator for the rest. What this feed is genuinely good at is being the reference leg: the sharp line everything else gets mapped against. The soft-book side of the pipeline is yours to build and yours to babysit.

No historical odds archive. The feed is real-time and pushed. If you want to backtest mapping logic against three seasons of closing lines, you record them yourself, starting now.

Alternate lines are not a ladder you can interpolate. You will be tempted to synthesise -0.85 between -0.75 and -1.0. Don't. Every line Pinnacle prices comes through, quarter lines included — soccer totals at 1.75 / 2.25 / 2.75 / 3.25 and quarter-ball handicaps at -0.75 / -1.25 / -1.75 / -2.25 — so use the real ones. Anything you invent between them is your model's opinion, and it should not live in the same table as market data.

Doing it in code

The shape I'd actually ship. to_home_anchored takes whatever a feed handed you and returns a home-anchored line, or dies.

from dataclasses import dataclass

@dataclass(frozen=True)
class AHLine:
    event_id: str
    period: str          # "ft", "1h", "2h"
    home_line: float     # ALWAYS relative to home
    home_price: float
    away_price: float

def to_home_anchored(side: str, line: float) -> float:
    """side is the team the quoted handicap belongs to."""
    if side == "home":
        return line
    if side == "away":
        return -line
    raise ValueError(f"unanchored handicap: {side!r}")

def is_quarter(line: float) -> bool:
    return abs(line * 4) % 2 == 1        # the .25 / .75 family

def split_quarter(line: float) -> tuple[float, float]:
    """-0.75 -> (-0.5, -1.0). Half stake on each."""
    if not is_quarter(line):
        raise ValueError("not a quarter line")
    return (line + 0.25, line - 0.25)

Two choices there are worth defending. First, raising on an unknown side instead of defaulting to home: a loud crash in the ETL is cheaper than a silent sign flip in production, and you will never notice the silent one until you're down. Second, is_quarter multiplies by four rather than matching on string suffixes, because -0.75 arriving from a JSON parser as -0.7500000001 is a thing that happens and string matching will quietly drop that row.

Pulling the reference leg is one call. Live markets:

curl -H "x-api-key: YOUR_KEY" \
  "https://pinnodds.com/kit/v1/markets?event_type=live"

Prematch handicaps sit under /kit/v1/prematch/lines, with the fixture list at /kit/v1/prematch/fixtures. Exact field names and response shapes are in the docs. Both official SDKs — npm install pinnodds and pip install pinnodds — wrap the REST and SSE surface, so the HTTP layer isn't code you need to own.

On the streaming side, the raw WebSocket passthrough at /ws/feed pushes updates instead of making you poll. That matters more here than it first appears: a handicap moves as a pair — line and price together. Polling gives you snapshots, and a snapshot taken after the pair has moved tells you nothing about the transition. Push gives you the transition, which is the thing your comparator actually cares about.

A worked example

Pinnacle, via /kit/v1/prematch/lines, on a match with Arsenal at home:

  • Arsenal -1.25 at 1.95, Chelsea +1.25 at 1.97

A second feed hands you {"team": "Chelsea", "handicap": 1.25, "price": 2.10}.

Naive comparison: "1.25 versus 1.25, and 2.10 beats 1.97 — value on Chelsea." That conclusion is correct, but only by luck, because you never established the anchor. Through the mapper it becomes home_line: -1.25, away_price: 2.10 against a reference of home_line: -1.25, away_price: 1.97. Same line, same side, real price gap. Now it's a signal you can size.

Flip the second feed's row to {"team": "Arsenal", "handicap": 1.25} — a book exporting the underdog-style line against the favourite because a trader fat-fingered it, or because their CSV writes the away handicap under the home label. Naive comparison still cheerfully reports "1.25 versus 1.25". The mapper produces home_line: +1.25, finds no reference row at +1.25, and refuses to pair. That is the whole point of anchoring: mismatches degrade into non-matches instead of false positives.

Log those non-matches. A sudden spike in unpairable rows from one source is usually the first sign that a feed changed its convention on you.

Where the drop feed fits

If you trigger comparisons off odds-drop alerts, you have a second normalisation job, because the SSE and REST surfaces name things differently on purpose.

An SSE alert from /odds-drop identifies the market in sect, the moving selection in outcome, prices in from_price / to_price, and the event in id. It also carries limit (Pinnacle's max stake) and nvp (the no-vig fair price) — but no drop percentage. The REST form at /api/drops is the enriched version: market, designation, from / to, event_id, plus a precomputed drop_pct.

Practically: outcome and designation are the fields your anchoring logic keys on. Treat them with exactly the paranoia you'd apply to a raw handicap side, because that's what they are.

Takeaway

Four rules, in order of how much pain they save. Store one line anchored to home and derive the other side on read. Crash loudly when a feed gives you a handicap without an unambiguous side rather than defaulting to anything. Scope every row by period before you compare it. And do fixture matching on kickoff-time-plus-normalised-name with an alias table for the stragglers — that's where the ongoing cost actually lives, not in the sign arithmetic.

Get those right and normalising spread data from multiple feeds stops manufacturing phantom edges. Line depth, quarter lines included, is identical on every plan — plans differ on rate limit and push access, not on what you can see — so once the mapping is solid the data side is done.

Frequently asked questions

How do I map Asian handicap lines between bookmakers without sign errors?

Anchor every handicap to one team — use the home side — and store only that number, deriving the away line as its negative on read. If a feed sends a handicap without clearly naming the side it belongs to, raise an error instead of guessing.

What is the difference between an Asian handicap and a European handicap?

An Asian handicap has two outcomes with no draw and allows quarter lines like -0.75 that split your stake. A European handicap has three outcomes — home, draw, away — with a whole-number handicap, so "home -1, draw" is a valid settlement that has no equivalent in Asian handicap markets.

Why does -0.75 exist as a handicap line?

It is a quarter line: your stake is split evenly across -0.5 and -1.0. A one-goal win therefore returns half a win and half a stake-back, which is why quarter lines can't be treated as a single half line.

Does pinnodds include quarter handicap lines?

Yes. Every line Pinnacle prices comes through, including quarter-ball handicaps at -0.75 / -1.25 / -1.75 / -2.25 and totals at 1.75 / 2.25 / 2.75 / 3.25. Depth is the same on every plan; plans differ in rate limit and push access.

Can I get historical Asian handicap odds from pinnodds?

No. The feed is real-time live and prematch data pushed over WebSocket and SSE, with no historical archive — if you want closing lines for backtesting, you need to record them yourself from the live feed.

Get real-time Pinnacle odds in your code

Live & prematch markets with instant odds-drop alerts over SSE and WebSocket. Free trial key in seconds — no card.

Start free trial