# Odds Format Conversion: Decimal, American, Fractional, Asian

Convert everything through decimal, store it at full precision, and convert to other formats only for display.

Odds format conversion is simple arithmetic: decimal odds are the total return per unit staked, fractional odds are profit over stake, and American odds are the profit on a 100 stake (positive) or the stake needed to win 100 (negative). Convert everything to decimal first, store it at full precision, and convert to other formats only for display. The formulas fit in one table below. The bugs come from rounding, floating point and averaging, and that is most of what this guide covers.

## The formulas, all routed through decimal

Decimal odds are the natural hub. [Wikipedia's fixed-odds betting article](https://en.wikipedia.org/wiki/Fixed-odds_betting) says decimal odds are favoured in continental Europe, Australia, New Zealand and Canada, while fractional odds are used primarily in the UK and Ireland, and moneyline (American) odds in the United States. If you convert every format to and from decimal, you write two functions per format instead of one per pair.

Let D be decimal odds.

| Format | From decimal | To decimal |
| --- | --- | --- |
| Implied probability | p = 1 / D | D = 1 / p |
| Fractional (a/b) | a/b = D - 1, then snap to a fraction | D = a/b + 1 |
| American, D of 2.00 or more | A = (D - 1) x 100 | D = A / 100 + 1 |
| American, D under 2.00 | A = 100 / (1 - D) | D = 100 / abs(A) + 1 |
| Hong Kong | H = D - 1 | D = H + 1 |
| Indonesian | I = A / 100 | via American |
| Malay, D of 2.00 or less | M = D - 1 | D = M + 1 |
| Malay, D over 2.00 | M = 1 / (1 - D) | D = 1 - 1 / M |

The fractional and American to decimal rules are the ones on [Wikipedia](https://en.wikipedia.org/wiki/Fixed-odds_betting). The Hong Kong, Malay and Indonesian conversions, and the decimal to American rule, are all implemented in the [R odds.converter package](https://search.r-project.org/CRAN/refmans/odds.converter/html/00Index.html), whose [source](https://github.com/cran/odds.converter/tree/master/R) shows each formula. It is a good reference to test your own functions against.

### The same five prices in every format

Calculated from the formulas above, rounded for display:

| Decimal | Fractional | American | Hong Kong | Malay | Indonesian | Implied % |
| --- | --- | --- | --- | --- | --- | --- |
| 1.50 | 1/2 | -200 | 0.50 | 0.50 | -2.00 | 66.67 |
| 1.91 | about 10/11 | -110 | 0.91 | 0.91 | -1.10 | 52.36 |
| 2.00 | 1/1 (evens) | +100 | 1.00 | 1.00 | 1.00 | 50.00 |
| 2.50 | 6/4 | +150 | 1.50 | -0.67 | 1.50 | 40.00 |
| 3.40 | 12/5 | +240 | 2.40 | -0.42 | 2.40 | 29.41 |

Notice two things. Malay odds change sign at 2.00 in the opposite direction to American and Indonesian odds, which is the single most common source of sign bugs. And 1.91 is "about" 10/11, because 10/11 is really 1.90909..., which leads straight into rounding.

## Five conversion bugs that show up in real code

### 1. Truncating floats instead of rounding them

Most decimal fractions cannot be stored exactly in binary floating point, as the [Python floating-point tutorial](https://docs.python.org/3/tutorial/floatingpoint.html) explains. JavaScript has the same limit: every [Number is an IEEE 754 double](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) with about 15 to 17 significant decimal digits.

Here is where that bites. A UK price of 5/6 is decimal 1.8333..., which should be exactly -120 in American odds. In Python using ordinary floats, `100 / (1 - (1 + 5/6))` comes out as -119.99999999999999. Pass that to `int()` and you publish -119. The fix:

- Round to the nearest integer for American odds, never truncate.
- For exact work (settlement, ledgers), use [Python's decimal module](https://docs.python.org/3/library/decimal.html) or exact fractions. The Python docs say decimal "is preferred in accounting applications which have strict equality invariants".
- Compare floats with a tolerance, not with equality.

### 2. Round-tripping through a rounded value

A US price of -110 is decimal 1.909090... Displayed to two places it becomes 1.91. If your pipeline stores the displayed 1.91 and later converts back, you get -109.89, which rounds back to -110. That looks safe, but the stored probability has moved:

- From -110 exactly: implied probability 52.381%, and a two-way market at -110 on both sides carries a margin of 4.762%.
- From the rounded 1.91: implied probability 52.356%, and the same market now shows a margin of 4.712%.

Five hundredths of a percentage point sounds small until you are ranking prices across a dozen bookmakers, removing the margin to estimate fair odds, or setting a threshold where one book's price edges past another's. Rounding error then decides which book "wins". Keep the unrounded value, and if your source quoted another format, keep its original string (such as "-110" or "10/11") so you can always recompute. If you are removing margins, our guide to [devig methods compared](https://sockodds.com/blog/remove-bookmaker-margin-devig-methods/) shows how sensitive each method is to the inputs.

### 3. The hole between -100 and +100

American odds have no valid values strictly between -100 and +100, and -100 and +100 are the same price (decimal 2.00). Your code should:

- Reject or flag any American value with an absolute value under 100.
- Normalise -100 to +100 so equal prices compare as equal.
- Reject decimal odds of 1.00 or below. Decimal 1.00 also divides by zero in the D under 2.00 American formula.

### 4. Averaging American odds directly

The average of -110 and +110 is 0, which is not a price. The average of -200 and +200 is also 0. Averages in American odds are meaningless across the sign change and distorted even within one side.

Average in probability space instead. The probabilities for -110 and +110 are 52.381% and 47.619%. Their mean is 50.000%, which is decimal 2.00, or +100. The same rule applies when you build a [consensus price](https://sockodds.com/blog/consensus-odds-calculation/) from several bookmakers: convert each price to a probability, combine, then convert back for display.

### 5. Turning decimals into fractions the naive way

Converting 1.91 to a fraction mechanically gives 91/100. No UK bookmaker would show that. Fractional odds are quoted from a conventional set of fractions like 10/11, 5/6, 4/5 and 1/2, so a decimal to fractional converter needs a lookup table of the fractions you are willing to display and should snap to the nearest one, ideally the nearest one that does not overstate the price. Then label the result as approximate, because 10/11 (1.909) and 1.91 are not the same number.

## What SockOdds returns, and converting at the edge

SockOdds API v1 returns one format: `decimal_odds` on each bookmaker's price, exactly as the bookmaker showed it, never converted, rounded or adjusted. Beside it sits `implied_probability`, which is 1 divided by `decimal_odds` to six places, with the bookmaker's margin still in it. There is no American or fractional field, so if your users want another format, you convert in your display layer. That keeps the stored number canonical, which is the pattern this guide recommends anyway.

This program reads one NFL game's head to head from two bookmakers and prints each price in decimal, American (rounded half away from zero, never truncated) and as the implied probability the API returned:

```javascript
// Round half away from zero, the same way in every language. Never truncate.
function american(decimal) {
  const raw = decimal >= 2 ? (decimal - 1) * 100 : 100 / (1 - decimal);
  const rounded = Math.sign(raw) * Math.floor(Math.abs(raw) + 0.5);
  return rounded > 0 ? `+${rounded}` : `${rounded}`;
}

const url = "https://api.sockodds.com/v1/events/ev_000ckk0ee4ze/odds?markets=winner&bookmakers=sportsbet,tab";
const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` } });
const { data: event } = await response.json();
console.log(event.name);
for (const market of event.markets) {
  for (const o of market.outcomes) {
    for (const price of o.prices) {
      const percent = (price.implied_probability * 100).toFixed(2);
      console.log(`  ${price.bookmaker.padEnd(10)} ${o.name.padEnd(20)} ${price.decimal_odds.toFixed(2)}  ${american(price.decimal_odds).padStart(5)}  ${percent}%`);
    }
  }
}
```

```python
import json, math, os, urllib.request


# Round half away from zero, the same way in every language. Never truncate.
def american(decimal):
    raw = (decimal - 1) * 100 if decimal >= 2 else 100 / (1 - decimal)
    rounded = int(math.copysign(math.floor(abs(raw) + 0.5), raw))
    return f"+{rounded}" if rounded > 0 else f"{rounded}"


url = "https://api.sockodds.com/v1/events/ev_000ckk0ee4ze/odds?markets=winner&bookmakers=sportsbet,tab"
request = urllib.request.Request(url, headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]})
with urllib.request.urlopen(request) as response:
    event = json.load(response)["data"]
print(event["name"])
for market in event["markets"]:
    for o in market["outcomes"]:
        for price in o["prices"]:
            percent = f"{price['implied_probability'] * 100:.2f}"
            print(f"  {price['bookmaker'].ljust(10)} {o['name'].ljust(20)} {price['decimal_odds']:.2f}  {american(price['decimal_odds']).rjust(5)}  {percent}%")
```

Output (from a local copy of the API with invented test prices):

```text
Arizona Cardinals v Atlanta Falcons
  tab        Arizona Cardinals    1.77   -130  56.50%
  sportsbet  Arizona Cardinals    1.72   -139  58.14%
  tab        Atlanta Falcons      2.18   +118  45.87%
  sportsbet  Atlanta Falcons      2.12   +112  47.17%
```

The prices come from a local copy of the API with invented numbers.

## Prediction market prices are already probabilities

Prediction markets skip odds formats entirely. On Kalshi a contract's price reflects the market's view of the probability of the outcome, and a "Yes" buyer and a "No" buyer together put in $1, as [Kalshi's help centre](https://help.kalshi.com/markets/markets-101/how-are-prices-determined) explains. Polymarket [displays the midpoint of the order book's bid and ask](https://help.polymarket.com/en/articles/13364488-how-are-prices-calculated), falling back to the last traded price when the spread is wider than 10 cents.

To put either beside a bookmaker price:

1. Treat the price in dollars as p. A 62 cent contract is p = 0.62.
2. Decimal odds are 1 / p, so 1 / 0.62 is about 1.613.
3. Decide whether you use the bid, the ask, the midpoint or the last trade, and record which. Each gives a different decimal price.
4. Remember trading fees are not in the quoted price.

In SockOdds, Kalshi and Polymarket arrive as bookmakers with their own keys and `decimal_odds`, on the same markets and outcomes as the sportsbooks. We cover lining them up in [Kalshi and Polymarket prices next to Australian bookmakers](https://sockodds.com/blog/prediction-market-api-guide/).

## A storage pattern that avoids all five bugs

For an odds comparison site, a model or an alerting tool, this pattern holds up:

1. **One canonical number.** Store decimal odds (or implied probability) at full precision. Never store a display-rounded value as the source of truth.
2. **Keep the original.** If a source quotes another format, store its own string and format next to the canonical number.
3. **Convert at the edge.** Produce American, fractional or Asian formats only in the display layer, rounded once, with the right rounding rule for each format.
4. **Validate on the way in.** Reject decimal odds of 1.00 or below and American odds between -100 and +100, and normalise -100 to +100.
5. **Do maths in probability space.** Averages, consensus prices, margin removal and comparisons all go through implied probability.
6. **Test against a reference.** Run your converters against a known library such as [odds.converter](https://search.r-project.org/CRAN/refmans/odds.converter/html/00Index.html) across a grid of prices, including the edges at 1.01, 2.00 and very long odds.

If you are building the comparison side, [How to Build an Odds Comparison Site in Australia](https://sockodds.com/blog/build-odds-comparison-site-australia/) walks through the rest of the pipeline.

## Questions people ask

### How do you convert American odds to decimal?

For positive American odds, divide by 100 and add 1, so +150 is 2.50. For negative American odds, divide 100 by the absolute value and add 1, so -150 is 1.667.

### Which odds format should an odds API store?

Store decimal odds (or implied probability) at full precision as the one canonical value, keep any original string beside it, and convert to other formats only when you display them. SockOdds returns decimal only, for that reason.

### Why does converting 5/6 to American odds give -119 in my code?

In 64-bit floating point, 5/6 converted to American odds comes out as -119.99999999999999. Truncating with `int()` gives -119. Round to the nearest integer instead, or do the arithmetic with exact fractions or a decimal type.

### How do Hong Kong, Malay and Indonesian odds relate to decimal?

Hong Kong odds are decimal minus 1. Indonesian odds are American odds divided by 100. Malay odds are decimal minus 1 when decimal is 2.00 or under, and -1 divided by (decimal minus 1) above 2.00.

### How do you turn a Kalshi or Polymarket price into decimal odds?

The price in dollars is the implied probability, so divide 1 by it. A 62 cent contract is 1 / 0.62, about 1.61 in decimal odds, before any trading fees.

## Sources

- [Wikipedia: Fixed-odds betting](https://en.wikipedia.org/wiki/Fixed-odds_betting)
- [CRAN: odds.converter package reference](https://search.r-project.org/CRAN/refmans/odds.converter/html/00Index.html)
- [odds.converter source, CRAN's copy on GitHub](https://github.com/cran/odds.converter/tree/master/R)
- [Python docs: Floating-Point Arithmetic, Issues and Limitations](https://docs.python.org/3/tutorial/floatingpoint.html)
- [Python docs: decimal module](https://docs.python.org/3/library/decimal.html)
- [MDN: Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)
- [Kalshi Help: How are prices determined?](https://help.kalshi.com/markets/markets-101/how-are-prices-determined)
- [Polymarket Help: How are prices calculated?](https://help.polymarket.com/en/articles/13364488-how-are-prices-calculated)

## Next

- [Reading prices](https://sockodds.com/docs/odds/)
- [Get a free Developer key](https://sockodds.com/signup/) and run your converters on real bookmaker prices
- [Pricing](https://sockodds.com/pricing/)
- [Odds API rate limits](https://sockodds.com/blog/odds-api-rate-limits/)
- [Live sports odds API freshness](https://sockodds.com/blog/live-sports-odds-api-freshness/)

This guide is for developers building data tools, not betting advice. Betting odds describe prices, not outcomes. 18+ only. Please gamble responsibly.

SockOdds provides data only. It does not offer tips or predictions on any game.
