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 input | Profit per 1 staked | Decimal calculation | Decimal output |
|---|---|---|---|
1/2 | 0.50 | 1 / 2 + 1 | 1.50 |
evens (1/1) | 1.00 | 1 / 1 + 1 | 2.00 |
5/2 | 2.50 | 5 / 2 + 1 | 3.50 |
11/4 | 2.75 | 11 / 4 + 1 | 3.75 |
100/30 | 3.333… | 100 / 30 + 1 | 4.333… |
4/9 | 0.444… | 4 / 9 + 1 | 1.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:
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.
Require an integer numerator, a positive integer denominator and a decimal result at least 1.0. A zero denominator is always an error.
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.
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.
| Input | Interpretation | Decimal result | Parser rule |
|---|---|---|---|
11/4, 11-4, 11 4 | Ordinary fractional price | 3.75 | Normalize the separator, then apply the formula |
evens, evs, even, 1/1 | One unit profit for one unit staked | 2.00 | Map accepted aliases to 1/1 |
2/1 on | Odds-on notation: invert to 1/2 | 1.50 | Apply inversion only for an explicit supported suffix |
9/2f | Fractional quote with a favourite marker | 5.50 | Strip the marker only; it is not part of the price |
SP | Starting price, not known at parse time | none | Return a tagged non-price value or null |
NR / withdrawn | Not a priced selection | none | Keep 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.
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.
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:
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.
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.
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.
/kit/v1/markets?sport_id=N returns current live markets with decimal prices and a generated_at timestamp.
/kit/v1/prematch/fixtures?sport_id=N and event lines provide decimal prematch prices for models and screens.
/api/drops and eligible-plan SSE expose detected price drops, with nvp as a separate no-vig reference when available.
The optional PinnWire WebSocket carries live and prematch market updates when your local book needs every change.
Verify the live surface immediately:
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 need | PinnWire surface | Why decimal-first helps |
|---|---|---|
| Current live pricing | /kit/v1/markets | Use returned decimal prices directly in calculations |
| Prematch model input | /kit/v1/prematch/fixtures or /kit/v1/prematch/lines | Keep line identity and decimal price as separate typed fields |
| Line-movement alerts | REST or SSE drops | Avoid fractional rounding creating false movements |
| Every market update | Optional raw WebSocket | Maintain a local decimal market book and render later |
| AI-assisted odds workflows | PinnWire MCP server | Agents 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.
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
- Forgetting the plus one.
11/4is 2.75 profit but 3.75 total-return decimal. - Treating evens as decimal 1.00. Evens is
1/1, so the total-return decimal is 2.00. - Returning a number for SP. SP has no known price at parse time; model it as a status.
- Missing the odds-on inversion. An explicit
2/1 onmeans1/2, not 3.00. - Reducing a display ladder back into storage. A snapped fraction can be a rounded approximation and create phantom movement.
- Converting a market line. Totals and handicaps are contract fields, not fractional prices.
- 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.