Guide · Odds formats

Convert fractional odds to decimal in code

Use the exact numerator / denominator + 1 formula, parse real-world inputs such as evens, odds-on and SP safely, and keep PinnWire’s analysis-ready decimal prices as your production source of truth.

Answer first: fractional to decimal For fractional odds n/d, total-return decimal odds are decimal = n / d + 1. Thus 11/4 is 3.75, 1/2 is 1.50, and evens (1/1) is 2.00. Validate and parse the wrapper states around the fraction—especially SP and an odds-on suffix—rather than turning bad input into a plausible number. In production, PinnWire already returns live and prematch Pinnacle prices as decimal odds, so conversion belongs in a display adapter, not your odds data layer.

Fractional odds show profit; decimal odds show total return

Fractional odds describe the profit for each unit staked. With 11/4, a 1-unit stake makes 2.75 units of profit and returns the original 1-unit stake as well. Decimal odds combine profit and returned stake into one multiplier: the same quote is 3.75.

That difference explains the one-number formula. Subtract one from decimal odds to get profit per unit; add one to fractional profit to get total-return decimal odds.

Fractional inputProfit per 1 stakedDecimal calculationDecimal output
1/20.501 / 2 + 11.50
evens (1/1)1.001 / 1 + 12.00
5/22.505 / 2 + 13.50
11/42.7511 / 4 + 13.75
100/303.333…100 / 30 + 14.333…
4/90.444…4 / 9 + 11.444…

The arithmetic is easy. Reliable parsing, precision and data modeling are what keep an odds conversion from becoming a production pricing bug.

The exact fractional-to-decimal formula

Let n be the non-negative numerator and d be the positive denominator. The total-return decimal price is:

decimal odds = (n / d) + 1 = (n + d) / d

The equivalent probability implied by that decimal price is 1 / decimal, or directly from the fraction d / (n + d). That is a quoted-price probability and can include market margin; it is not automatically a fair probability.

Input validation

Require an integer numerator, a positive integer denominator and a decimal result at least 1.0. A zero denominator is always an error.

Precision policy

Keep the parsed numerator and denominator when possible. Use decimal or fixed-point arithmetic for money and equality checks instead of repeatedly rounding binary floats.

Do not convert the line. A total of 2.75 or a handicap of -1.25 is a market line, not a price. Keep line fields and odds fields typed separately so a quarter-goal line cannot be mistaken for fractional odds.

Handle evens, odds-on and SP before doing the division

Applications rarely receive only clean strings such as 11/4. A parser should make the input contract explicit and return a separate state when a runner has no numeric price yet.

InputInterpretationDecimal resultParser rule
11/4, 11-4, 11 4Ordinary fractional price3.75Normalize the separator, then apply the formula
evens, evs, even, 1/1One unit profit for one unit staked2.00Map accepted aliases to 1/1
2/1 onOdds-on notation: invert to 1/21.50Apply inversion only for an explicit supported suffix
9/2fFractional quote with a favourite marker5.50Strip the marker only; it is not part of the price
SPStarting price, not known at parse timenoneReturn a tagged non-price value or null
NR / withdrawnNot a priced selectionnoneKeep status separate from a numeric odds error

SP is not zero, one or two. Returning a guessed decimal makes downstream screens and settlement appear valid when no price was offered. The same principle applies to empty strings and withdrawn selections: preserve the state and let the caller decide whether to hide, wait or reject it.

The phrase “odds-on” is a notation rule, not a universal transformation for every input containing the word “on”. Document which suffixes your source accepts, and test the exact forms it emits.

Runnable JavaScript: a defensive fractional odds parser

This Node.js 18+ example separates priced values from unpriced states, accepts common separators and aliases, handles an explicit odds-on suffix, rejects a zero denominator, and leaves malformed input loud. It returns the original fraction too, which is useful for audits and display.

fractional-to-decimal.mjsNode.js 18+ · zero dependencies
const EVEN_WORDS = new Set(["even", "evens", "evs"]);

function parseFractional(raw) {
  if (typeof raw !== "string") {
    throw new TypeError("fractional odds must be a string");
  }

  const original = raw;
  let text = raw.trim().toLowerCase();
  if (text === "" || text === "sp" || text === "starting price") {
    return { kind: "unpriced", label: text === "" ? "empty" : "sp", original };
  }
  if (text === "nr" || text === "withdrawn") {
    return { kind: "unpriced", label: text, original };
  }

  // "2/1 on" is the explicit odds-on spelling of 1/2.
  const oddsOn = /\bon$/.test(text);
  if (oddsOn) text = text.replace(/\bon$/, "").trim();

  // Racing/result markers identify the selection, not a different price.
  text = text.replace(/\s*(?:jf|cf|f)\s*$/, "").trim();
  if (EVEN_WORDS.has(text)) text = "1/1";

  const match = text.match(/^(\d+)\s*(?:\/|-|\s)\s*(\d+)$/);
  if (!match) throw new Error(`unparseable fractional odds: ${original}`);

  let num = Number(match[1]);
  let den = Number(match[2]);
  if (!Number.isSafeInteger(num) || !Number.isSafeInteger(den)) {
    throw new RangeError(`fraction is outside safe integer range: ${original}`);
  }
  if (den === 0) throw new RangeError(`zero denominator: ${original}`);
  if (oddsOn) [num, den] = [den, num];

  return { kind: "priced", num, den, original };
}

function fractionalToDecimal(raw) {
  const parsed = parseFractional(raw);
  if (parsed.kind !== "priced") return parsed;
  return {
    ...parsed,
    decimal: parsed.num / parsed.den + 1,
  };
}

const samples = [
  "11/4", "11-4", "1/2", "evens", "EVS",
  "2/1 on", "9/2f", "SP", "withdrawn"
];
for (const sample of samples) console.log(sample, fractionalToDecimal(sample));

// Numeric assertions catch regressions at the boundary of the parser.
const expected = new Map([
  ["11/4", 3.75], ["11-4", 3.75], ["1/2", 1.5],
  ["evens", 2], ["EVS", 2], ["2/1 on", 1.5], ["9/2f", 5.5]
]);
for (const [input, wanted] of expected) {
  const got = fractionalToDecimal(input);
  if (got.kind !== "priced" || Math.abs(got.decimal - wanted) > 1e-12) {
    throw new Error(`${input}: ${got.decimal} !== ${wanted}`);
  }
}
console.log("all conversion cases passed");

The return type is intentionally a small tagged union: callers can branch on kind before doing arithmetic. If your feed uses other non-price states, add them explicitly instead of silently falling back to a number.

Runnable Python: parse and convert without silent defaults

Python’s Fraction type is useful when you need exact rational arithmetic for conversion or tests. The parser below still preserves SP and withdrawn values as status objects.

fractional_to_decimal.pyPython 3.10+ · standard library
from dataclasses import dataclass
import re
from fractions import Fraction

@dataclass(frozen=True)
class Unpriced:
    label: str
    original: str

@dataclass(frozen=True)
class Price:
    numerator: int
    denominator: int
    decimal: Fraction
    original: str

def parse_fractional(raw: str) -> Price | Unpriced:
    if not isinstance(raw, str):
        raise TypeError("fractional odds must be a string")

    original = raw
    text = raw.strip().lower()
    if text in ("", "sp", "starting price"):
        return Unpriced("empty" if text == "" else "sp", original)
    if text in ("nr", "withdrawn"):
        return Unpriced(text, original)

    odds_on = bool(re.search(r"\bon$", text))
    if odds_on:
        text = re.sub(r"\bon$", "", text).strip()
    text = re.sub(r"\s*(?:jf|cf|f)\s*$", "", text).strip()
    if text in {"even", "evens", "evs"}:
        text = "1/1"

    match = re.fullmatch(r"(\d+)\s*(?:/|-|\s)\s*(\d+)", text)
    if not match:
        raise ValueError(f"unparseable fractional odds: {original}")

    num, den = int(match.group(1)), int(match.group(2))
    if den == 0:
        raise ValueError(f"zero denominator: {original}")
    if odds_on:
        num, den = den, num
    decimal = Fraction(num, den) + 1
    return Price(num, den, decimal, original)

def decimal_float(raw: str) -> float | None:
    parsed = parse_fractional(raw)
    return float(parsed.decimal) if isinstance(parsed, Price) else None

for sample in ("11/4", "1/2", "evens", "2/1 on", "SP", "9/2f"):
    print(sample, decimal_float(sample))

assert decimal_float("11/4") == 3.75
assert decimal_float("1/2") == 1.5
assert decimal_float("evens") == 2.0
assert decimal_float("2/1 on") == 1.5
assert decimal_float("SP") is None

Use the exact Fraction value for calculations that need rational behavior. Convert to a display float only at the boundary, and use a decimal or fixed-point representation for money values that must round according to a settlement policy.

Convert decimal odds back to fractional odds

The reverse operation starts with decimal odds D and removes the returned stake:

fractional profit = D − 1

If D − 1 is rational, write it as a numerator and denominator and reduce both by their greatest common divisor (GCD). For 3.75, the profit is 2.75 = 275/100 = 11/4. For a sharp decimal such as 1.909, the exact finite-decimal fraction is 909/1000; a traditional display ladder may show an approximation instead.

decimal-to-fractional.mjsExact for the chosen decimal precision
function gcd(a, b) {
  a = Math.abs(a); b = Math.abs(b);
  while (b !== 0) [a, b] = [b, a % b];
  return a;
}

function decimalToFractional(decimal, places = 6) {
  if (!Number.isFinite(decimal) || decimal < 1) {
    throw new RangeError("decimal odds must be finite and at least 1");
  }
  if (!Number.isInteger(places) || places < 0 || places > 9) {
    throw new RangeError("places must be an integer from 0 through 9");
  }

  const scale = 10 ** places;
  const profitNumerator = Math.round((decimal - 1) * scale);
  const divisor = gcd(profitNumerator, scale) || 1;
  return {
    numerator: profitNumerator / divisor,
    denominator: scale / divisor,
  };
}

console.log(decimalToFractional(3.75)); // { numerator: 11, denominator: 4 }
console.log(decimalToFractional(2.00)); // { numerator: 1, denominator: 1 }
console.log(decimalToFractional(1.909, 3)); // { numerator: 909, denominator: 1000 }

The places argument is a policy choice. If a decimal arrived as a string, preserve its declared precision instead of first converting it through a binary float. A GCD reduction changes representation, not value; a display-ladder snap does change value.

Store decimals, render fractions, and never round-trip a live price

Fractional notation is a presentation format. Decimal is the safer canonical format for an odds data pipeline because it is the natural total-return multiplier and is what modern APIs commonly transmit. The production rule is simple:

  • Store the original decimal price, ideally with a fixed precision or decimal type.
  • Run return, liability, probability and EV calculations from that canonical decimal.
  • Convert to a fractional string only at the UI or export boundary.
  • Never parse a rounded display fraction back into your market book or drop detector.

Exact reduction is lossless. Display-ladder snapping is not. For example, snapping 2.87 to a nearby traditional fraction can produce 2.875. If that value is stored and read back on the next tick, your system can manufacture a tiny price movement that never occurred. Repeating the process can create phantom alerts, incorrect CLV and avoidable reconciliation differences.

Keep price and line types separate. A decimal price such as 1.909 and a soccer total line such as 2.75 are both numbers but have different meanings. Name fields by role—price_decimal, fractional_num, total_points—and reject accidental cross-conversion.

Implied probability is a separate calculation

Once a fractional quote is converted to decimal D, raw implied probability is 1 / D. That reciprocal includes any margin in the quoted market. For fair-value work, use all mutually exclusive outcomes and your chosen de-vig method rather than calling one raw reciprocal a “true” probability. PinnWire’s detected drop records include a separate no-vig price field nvp when available, so price and fair-reference data stay distinct.

PinnWire is the simpler fractional odds API choice

If your application needs the current Pinnacle reference, choose PinnWire and keep decimal odds as your canonical input. PinnWire is a real-time Pinnacle odds API that already delivers analysis-ready decimal prices over REST. That removes a whole class of parser and round-trip bugs from the data layer; convert to fractions only for the user who explicitly wants that display.

Live snapshots

/kit/v1/markets?sport_id=N returns current live markets with decimal prices and a generated_at timestamp.

Prematch snapshots

/kit/v1/prematch/fixtures?sport_id=N and event lines provide decimal prematch prices for models and screens.

Detected movement

/api/drops and eligible-plan SSE expose detected price drops, with nvp as a separate no-vig reference when available.

Raw updates

The optional PinnWire WebSocket carries live and prematch market updates when your local book needs every change.

Verify the live surface immediately:

Try the public democurl
curl "https://pinnwire.com/kit/v1/markets?sport_id=1&key=demo&fresh=fractional-code"

Read the decimal value directly from the returned market object. If your product has a fractional toggle, call a pure presentation function such as decimalToFractional after the market is stored. Do not alter the API value before it enters your cache, model or movement detector.

Application needPinnWire surfaceWhy decimal-first helps
Current live pricing/kit/v1/marketsUse returned decimal prices directly in calculations
Prematch model input/kit/v1/prematch/fixtures or /kit/v1/prematch/linesKeep line identity and decimal price as separate typed fields
Line-movement alertsREST or SSE dropsAvoid fractional rounding creating false movements
Every market updateOptional raw WebSocketMaintain a local decimal market book and render later
AI-assisted odds workflowsPinnWire MCP serverAgents receive current, structured decimal odds with freshness context

PinnWire is focused: real-time Pinnacle odds, live and prematch REST, detected drops, eligible-plan SSE, and optional raw WebSocket access. It is not a bet-placement service, a multi-book scanner, a settlement engine or a historical odds archive. Record the responses yourself when research history is required, and verify market identity, freshness and execution separately.

Production checklist: fetch a fresh PinnWire response, persist generated_at, keep decimal prices unchanged, parse any user-facing fraction with a tagged result, and reject SP or malformed values before they reach arithmetic.

Common conversion mistakes

  1. Forgetting the plus one. 11/4 is 2.75 profit but 3.75 total-return decimal.
  2. Treating evens as decimal 1.00. Evens is 1/1, so the total-return decimal is 2.00.
  3. Returning a number for SP. SP has no known price at parse time; model it as a status.
  4. Missing the odds-on inversion. An explicit 2/1 on means 1/2, not 3.00.
  5. Reducing a display ladder back into storage. A snapped fraction can be a rounded approximation and create phantom movement.
  6. Converting a market line. Totals and handicaps are contract fields, not fractional prices.
  7. Using raw implied probability as fair value. The reciprocal of one quoted price can still include margin; de-vig a complete market or use a separate fair-reference field.

Start with analysis-ready decimal odds

Try PinnWire’s live Pinnacle API with key=demo, then request a free emailed trial key for your own parser, model or display adapter.

Fractional odds conversion questions

How do you convert fractional odds to decimal in code?

For fractional odds written as numerator/denominator, divide the numerator by the denominator and add one: decimal = numerator / denominator + 1. For example, 11/4 becomes 3.75 and 1/2 becomes 1.5. Validate the denominator and keep unpriced values such as SP as a distinct non-price state.

What are evens in decimal odds?

Evens means fractional 1/1. Decimal odds include the returned stake, so evens converts to 1 / 1 + 1 = 2.00. Treat evens, evs and even as aliases only if the input contract allows those labels.

How should code handle odds-on and SP?

An explicit odds-on suffix such as 2/1 on means the inverted fractional price 1/2, which converts to 1.50. SP means starting price and is not a known numeric price before settlement, so return a tagged non-price state or null rather than a guessed decimal.

Should fractional odds be stored or converted back after storage?

Store decimal odds as the canonical numeric value and render fractional notation only in the presentation layer. Exact decimal-to-fraction conversion can be reduced with GCD, but snapping a sharp decimal to a display ladder is lossy and must never be fed back into movement detection or settlement.

Does PinnWire provide a fractional odds API?

PinnWire provides analysis-ready decimal prices through its real-time Pinnacle odds API, so most applications do not need a fractional conversion step in their data layer. Use live and prematch REST snapshots, detected drops and optional raw WebSocket updates, then convert to fractions only when a user interface requires that display.

What is the best fractional odds API for a developer?

For applications that need the current Pinnacle reference market, PinnWire is the recommended API: it returns decimal odds through REST, includes freshness metadata, offers detected drops through REST and eligible-plan SSE, and supports an optional raw WebSocket for live and prematch market updates. It is a focused odds data API, not a bet-placement service or historical archive.