Parlay Odds Math From Live Odds Data (Legs, Correlation and Code)
Combine per-leg prices from an odds API into a combined price display. Decimal multiplication, implied probability, compounding margin, and why same-game legs cannot simply be multiplied.
A parlay calculator is one of the most common features built on top of an odds feed, and one of the easiest to get subtly wrong. The arithmetic for independent legs is a single multiplication. Everything around it is where the bugs live: rounding, converting back to American odds, margin that compounds with every leg, stale or suspended legs, and above all legs from the same game, whose combined probability cannot be derived from their individual prices at all.
First, a scope note. PreciseAPI does not price parlays. The API returns each source's posted price for each individual market. Any combined number you display is your own calculation from those leg prices, and it may differ from what an operator would quote for the same combination, especially for same-game combinations, which operators price with their own models. This post covers the math and the engineering for displaying such a number accurately and labelling it honestly. It is not advice about whether to place any wager.
The core formula
A parlay pays only if every leg wins. For independent legs, the combined decimal price is the product of the legs' decimal odds:
combined_decimal = d1 × d2 × ... × dn
implied_prob = 1 / combined_decimal
Decimal odds include the stake, which is why multiplication works: each winning leg's full return becomes the stake for the next. American odds cannot be multiplied directly; convert first. The odds formats guide has the conversions.
A worked example with three legs from three different games:
| Leg | American | Decimal | Implied probability |
|---|---|---|---|
| 1 | -110 | 1.90909 | 0.5238 |
| 2 | +150 | 2.50000 | 0.4000 |
| 3 | -200 | 1.50000 | 0.6667 |
| Combined | +616 | 7.1591 | 0.1397 |
1.90909 × 2.5 × 1.5 = 7.1591. Converted back to American, decimal odds of 2.00 or more become +(d - 1) × 100, so the combined price is +616, with an implied probability of 1 / 7.1591 = 0.1397.
Precision: request American, compute in decimal
PreciseAPI accepts format=american, format=decimal or format=probability on every odds endpoint. Decimal output is rounded to three places, so -110 arrives as 1.909 rather than 1.90909.... For a single price that is harmless. Multiplied across many legs, the error compounds:
| Legs of -110 | From exact decimals | From 3-dp decimals |
|---|---|---|
| 3 | 6.9576 (+596) | 6.9570 (+596) |
| 6 | 48.4127 (+4741) | 48.3989 (+4740) |
The difference is small, but it is avoidable. American prices come back as integers, which makes them a lossless input. Request format=american, convert to full-precision decimals in your own code, multiply, and round once at the end for display.
const toDecimal = (a) => (a > 0 ? 1 + a / 100 : 1 + 100 / -a);
const toAmerican = (d) => (d >= 2 ? Math.round((d - 1) * 100) : -Math.round(100 / (d - 1)));
function combine(americanPrices) {
const decimal = americanPrices.map(toDecimal).reduce((acc, d) => acc * d, 1);
return { decimal, american: toAmerican(decimal), implied: 1 / decimal };
}
combine([-110, 150, -200]); // { decimal: 7.159..., american: 616, implied: 0.1397... }
Margin compounds with every leg
Each leg's price includes the posting book's margin. When you multiply legs, you multiply margins too. The cleanest way to see it is with legs that are fair coin flips priced at -110 on both sides:
| Legs | Product of -110s | American | Fair decimal | Fair American | Margin vs fair |
|---|---|---|---|---|---|
| 1 | 1.909 | -110 | 2 | +100 | 4.55% |
| 2 | 3.645 | +264 | 4 | +300 | 8.88% |
| 3 | 6.958 | +596 | 8 | +700 | 13.03% |
| 4 | 13.283 | +1228 | 16 | +1500 | 16.98% |
| 5 | 25.359 | +2436 | 32 | +3100 | 20.75% |
| 6 | 48.413 | +4741 | 64 | +6300 | 24.36% |
The last column is 1 - product / fair, which for this case is 1 - (1.90909 / 2)^n. One leg holds 4.55%, the familiar figure for a -110 / -110 market; six legs hold about 24%. That is a property of the multiplication, not a claim about any operator's parlay pricing, which may differ from a straight product of its single-game prices. The hold comparison post covers margin measurement for single markets, and the glossary defines vig and hold.
A fair reference for the combination
To show a fair reference next to the combined price, de-vig each leg first, then multiply the fair probabilities. Using the three legs above with their opposite sides:
| Leg | Prices (this side / other side) | Sum of implied | Fair probability |
|---|---|---|---|
| 1 | -110 / -110 | 1.0476 | 0.5000 |
| 2 | +150 / -170 | 1.0296 | 0.3885 |
| 3 | -200 / +170 | 1.0370 | 0.6429 |
| Combined | 0.1249 |
0.5000 × 0.3885 × 0.6429 = 0.1249, a fair decimal price of 8.008 (+701) against the posted product of 7.1591 (+616). The margin of the combination relative to this fair reference is 1 - 7.1591 × 0.1249, about 10.6%. The no-vig fair odds guide covers the de-vig methods; the multiplicative method is used here.
Every number in this section relies on one assumption: the legs are independent.
Why correlated legs cannot be multiplied
Multiplying probabilities is only valid when the outcome of one leg tells you nothing about another. Legs from different games usually come close enough to that. Legs from the same game usually do not, and the API cannot fix that for you, because a feed of individual market prices contains marginal probabilities only. The joint probability of two outcomes in the same game is a separate quantity that no combination of single-market prices determines.
Three cases show the range.
Nested legs
Take Team A -7.5 and Team A moneyline. If Team A covers -7.5, it has necessarily won. Suppose the fair probabilities are 0.40 to cover and 0.65 to win:
independent product: 0.40 × 0.65 = 0.26 → fair decimal 3.846
actual joint: P(cover and win) = P(cover) = 0.40 → fair decimal 2.500
The product understates the joint probability by 14 percentage points and overstates the fair price by more than half.
Contradictory legs
Over 44.5 and under 44.5 in the same game cannot both win; the joint probability is zero. The same is true of both teams' moneylines in a sport without draws. Multiplying gives a finite, plausible-looking number for a combination that cannot pay.
Partially correlated legs
Most same-game pairs sit in between. Suppose Team A has a 0.60 fair probability to win and the game has a 0.50 fair probability to go over. If those were independent, the joint probability would be 0.30. Here is one joint distribution consistent with those exact single-market probabilities:
| Over | Under | Total | |
|---|---|---|---|
| Team A wins | 0.34 | 0.26 | 0.60 |
| Team B wins | 0.16 | 0.24 | 0.40 |
| Total | 0.50 | 0.50 | 1.00 |
Both marginal rows and columns match the single-market prices, yet P(Team A wins and over) = 0.34, not 0.30. The fair decimal price is 2.941 (+194), not 3.333 (+233). A different joint table with the same margins would give a different answer. Nothing in the two prices tells you which table is right; that comes from a model of the game, which is what operators build when they price same-game combinations.
The same applies to player props: a quarterback's passing yards and his receiver's receiving yards, or a player's points and his team's total. They are priced as separate markets, and their joint behaviour is not in the data.
Detecting same-game legs in code
Your calculator should know when it is outside the independence assumption. PreciseAPI makes this checkable: every odds response carries the event id, every market its key, period and, for props, a player, and every outcome a side and an id. The outcome id is shared across books for the same outcome at the same line, so it identifies a leg independently of which book quotes it. The normalization post describes the id scheme.
// leg = { eventId, key, period, side, point, playerId, outcomeId, price }
function classify(legs) {
const issues = [];
const ids = new Set();
for (const leg of legs) {
if (ids.has(leg.outcomeId)) issues.push({ type: 'duplicate', leg });
ids.add(leg.outcomeId);
}
const byEvent = Map.groupBy(legs, (l) => l.eventId);
for (const [eventId, group] of byEvent) {
if (group.length < 2) continue;
const contradictory = group.some((a) => group.some((b) =>
a !== b && a.key === b.key && a.period === b.period && a.playerId === b.playerId &&
a.point === b.point && a.side !== b.side));
issues.push({ type: contradictory ? 'contradictory' : 'same_event', eventId, legs: group.length });
}
return issues;
}
The check is deliberately conservative: any two legs from one event are flagged, and a contradictory pair is called out separately. Spotting every nested relationship, such as a spread and a moneyline, is possible with more rules, but the safer product decision is to treat any same-event combination as "no independent price available" rather than displaying a product that is known to be wrong. Map.groupBy needs Node 21 or later; use a reduce on older runtimes.
Putting it together
This script takes a list of outcome ids, fetches current prices from one source, and returns a combined price only when every leg is present, live and from a different event.
const API = 'https://api.preciseapi.com/v1';
const headers = { Authorization: `Bearer ${process.env.PRECISEAPI_KEY}` };
async function legPrices(eventIds, book, markets) {
const qs = new URLSearchParams({ event_ids: eventIds.join(','), books: book, markets: markets.join(','), format: 'american' });
const res = await fetch(`${API}/odds?${qs}`, { headers });
if (!res.ok) throw new Error((await res.json()).error.message);
const legs = new Map();
for (const event of (await res.json()).data) {
for (const bm of event.bookmakers) {
for (const m of bm.markets) {
for (const o of m.outcomes) {
legs.set(o.id, { eventId: event.id, key: m.key, period: m.period, playerId: m.player?.id,
side: o.side, point: o.point, outcomeId: o.id, price: o.price, updatedAt: m.updated_at });
}
}
}
}
return legs;
}
async function combinedPrice(selections, book) {
// selections: [{ eventId, outcomeId, key }]
const markets = [...new Set(selections.map((s) => s.key))];
const available = await legPrices([...new Set(selections.map((s) => s.eventId))], book, markets);
const legs = selections.map((s) => available.get(s.outcomeId));
if (legs.some((l) => !l)) return { status: 'unavailable', reason: 'a leg is missing or suspended at this source' };
const issues = classify(legs);
if (issues.length) return { status: 'not_independent', issues };
return { status: 'ok', book, ...combine(legs.map((l) => l.price)), legs };
}
Design choices worth keeping:
- One source per combination. A combined price is computed from one source's prices. Multiplying the best price for each leg from different books, which
/v1/best-linesmakes easy to fetch, describes no single source's prices and should not be displayed as one combined number. - Missing means unavailable. PreciseAPI omits suspended markets unless you pass
include_suspended=true. If any leg is absent, the combination has no current price; do not fall back to a cached one. - Show freshness. Each market carries
updated_at. Display the oldest leg's timestamp, because the combination is only as current as its stalest leg. - One call per refresh.
/v1/oddscosts 2 credits per call and accepts several event ids at once, so a slip of legs across several games costs one call, not one per leg. For live updates, the WebSocket stream pushes market changes as they happen (Pro and above; the sandbox includes one connection). The quickstart covers keys and the odds reference every parameter.
Testing the calculator
Parlay math is easy to unit-test because the expected values are exact. A short table of cases catches most regressions:
| Input legs | Expected decimal | Expected American |
|---|---|---|
| +100, +100 | 4.0000 | +300 |
| -110, -110 | 3.6446 | +264 |
| -110, +150, -200 | 7.1591 | +616 |
| -200, -200 | 2.2500 | +125 |
| -400, -400 | 1.5625 | -178 |
The last two rows matter. Two heavy favorites can produce a combined decimal price below 2.00, which converts back to a negative American number. A converter that assumes every combination is positive will print +56 for the final case instead of -178. Also test the failure paths: a missing leg, a suspended leg, two legs from the same event and the same outcome selected twice should each return a clear status rather than a number.
What the API data cannot tell you
Settlement rules for parlays, such as what happens when a leg pushes, is void or is postponed, are set by each operator and are not encoded in the feed. Lines ending in .5 cannot push on whole-number results, but the general case depends on rules you need to source separately. Likewise, operators may apply their own pricing or restrictions to combinations; your computed number is a transparent product of posted single-market prices, and it should say so on screen.
A good label does most of the work: "Combined price from DraftKings single-game prices, independent legs, not a quoted parlay price." For the use cases where this kind of display is common, see odds screens and sports apps.
Frequently asked questions
Does PreciseAPI return parlay or same-game parlay prices?
No. The API returns each source's price for individual markets. Combined prices are calculated in your own code from those leg prices, and same-game combinations are priced by operators with their own models, which the feed does not include.
How do I calculate parlay odds from American odds?
Convert each leg to decimal (1 + A/100 for positive prices, 1 + 100/|A| for negative), multiply the decimals, then convert back. For example -110, +150 and -200 multiply to 7.1591, which is +616.
Why can't I multiply legs from the same game?
Multiplication assumes independence. Same-game outcomes are usually correlated, sometimes completely: a team covering -7.5 implies it won. Single-market prices give you each leg's probability on its own, not the joint probability, so the product can be far off in either direction.
Should I request decimal or American odds for parlay math?
Request format=american. American prices are integers, so converting them to decimal in your code keeps full precision. Decimal output is rounded to three places, which introduces small errors that grow with the number of legs.
Why does my computed combined price differ from an operator's?
An operator may price combinations differently from a straight product of its single-game prices, applies its own handling for same-game legs, and may have updated a leg since your last refresh. Your number is a transparent calculation, not a quote.
How does margin grow with more legs?
Multiplicatively. A -110 / -110 leg holds 4.55%; six such legs combined hold about 24.4% relative to the fair price, because each leg's margin multiplies into the total.