# Mapping markets


Source: https://docs.stxapp.io/concepts/market-shape/

How a market on the exchange corresponds to a real game and a real outcome, and
which fields to key your own records on.

It covers the core game markets: winners, draws, totals, handicaps, and team and
event stat lines. Player props follow a different `specifier` grammar and are not
covered here; see [What this page does not cover](#what-this-page-does-not-cover).

For the symbol grammar see [Market symbols](/concepts/market-symbols/), and for
the lifecycle values see [Market and order status](/concepts/market-status/).

## Start here: one game, twelve markets, nine books

Markets do not have a "both sides" object. Each outcome is its own market with
its own order book. A real NCAAF game carries twelve, and those twelve form nine
sets of mutually exclusive outcomes: the moneyline pair is one set, the three
handicap lines are one set, and each stat question is its own.

`grouping_id` is the field that says which set a market belongs to. Every value
below is prefixed by the event's id, elided here as `{event}` to keep the table
readable.

| `rules` | `specifier` | `grouping_id` | Settles yes if |
| --- | --- | --- | --- |
| `home_winner` | `null` | `{event}:moneyline:full` | Duke win |
| `away_winner` | `null` | `{event}:moneyline:full` | Tulane win |
| `spread` | `"-4.5"` | `{event}:spread:full` | Duke win by more than 4.5 |
| `spread` | `"-7.5"` | `{event}:spread:full` | Duke win by more than 7.5 |
| `spread` | `"-13.5"` | `{event}:spread:full` | Duke win by more than 13.5 |
| `event_stat_line` | `"home\|FIRST_SCORE\|NA"` | `{event}:esl:full:FIRSTSCORE:HOME` | Duke score first |
| `event_stat_line` | `"away\|FIRST_SCORE\|NA"` | `{event}:esl:full:FIRSTSCORE:AWAY` | Tulane score first |
| `event_stat_line` | `"home\|LAST_SCORE\|NA"` | `{event}:esl:full:LASTSCORE:HOME` | Duke score last |
| `event_stat_line` | `"away\|LAST_SCORE\|NA"` | `{event}:esl:full:LASTSCORE:AWAY` | Tulane score last |
| `event_stat_line` | `"both\|FIRST_SCORE_TOUCHDOWN\|NA"` | `{event}:esl:full:FIRSTSCORETOUCHDOWN:BOTH` | The first score is a touchdown |
| `event_stat_line` | `"both\|ANY_SCORE\|39.5"` | `{event}:esl:full:ANYSCORE:BOTH` | Either team scores 40+ |
| `event_stat_line` | `"both\|OVERTIME\|NA"` | `{event}:esl:full:OVERTIME:BOTH` | The game goes to overtime |

Four things to take from that list.

**A moneyline is two markets**, not one market with two sides. Their books are
independent, and their prices need not sum to `max_price`. They share a
`grouping_id`, which is how you know they are the same question seen from both
ends.

**One `rules` value can appear several times on the same game**, at different
specifiers: three handicap lines above, all one set. `(event_id, rules)` does
not identify a market; `(event_id, rules, specifier)` does.

**A shared `grouping_id` does not identify a market either**; that is the
point of it. Five of the twelve markets above sit on a `grouping_id` they share
with another market.

**Not every family exists on every game.** That game has no totals market at
all. Build your mapping from what the API returns, not from a template of what
a game "should" have.

If that table makes sense, the rest of this page is detail. The three fields
below and the six steps after them are the whole job; everything later is
reference for the moments when one of those steps is not obvious.

---

## The three fields that map a market

Mapping has two levels, and conflating them is the most expensive mistake on
this page. Your system almost certainly holds an *instrument* ("the Duke/Tulane
handicap ladder") with several legs. Ours holds one market per leg. Three
fields carry that structure, and each answers a different question.

| Field | Question it answers | Unique per market? |
| --- | --- | --- |
| `grouping_id` | **Which set of mutually exclusive outcomes?** The book. | **No, deliberately**: siblings share it |
| `rules` + `specifier` | **Which outcome within that set?** The leg. | Yes, with `event_id` |
| `market_id` | **What do I send back to trade?** The handle. | Yes |

A complete mapping stores all three: `grouping_id` to line our book up against
your instrument, `rules` and `specifier` to line up the leg, `market_id` to act.

```json
{
  "market_id":     "0a5f9c31-6d24-4b17-9e83-c1f7a0d5b862",
  "event_id":      "272b75e2-77d2-408b-b9f6-d7f1090646fc",
  "grouping_id":   "272b75e2-77d2-408b-b9f6-d7f1090646fc:spread:full",
  "grouping_name": "Spread",
  "rules":         "spread",
  "specifier":     "-7.5"
}
```

### Why `grouping_id` is worth reading rather than deriving

For game markets you could work the set out yourself: the family and the scope
both follow from `rules`, and the strike from `specifier`. For season futures
you cannot. A `division_winner` market's set is its division, which comes from
the standings we hold and you do not, and the filter fields that used to carry
it are empty on every settled market. One NFL season event holds 96 markets in
11 sets; nothing else in the payload separates them.

`grouping_id` is the only part of the market relation that is not computable
from the rest of the payload. That is the reason it exists.

:::caution[`grouping_id` keys a set, not a market]
Two markets sharing a `grouping_id` is normal and intended, and common: every
moneyline pair, every three-way triple, and every line of a handicap or totals
ladder sits on one.

An integration that resolves its instrument to a `grouping_id` and then trades
"the market" it found will buy the draw when it meant the home side. Resolve to
the set, then pick the leg with `rules` and `specifier`.

```
grouping_id  {event}:3way:regulation   specifier  null   ->  three markets:
  home_winner_regulation_3way, away_winner_regulation_3way, draw_regulation_3way
```
:::

:::caution[Never join on `grouping_name`]
`grouping_name` is the set in words: `"Spread"`, `"AL East Division"`,
`"Last Run (BOS)"`. It is for display and for a human reading a log. It is not
a key. It is derived for display, so two different sets can render the same
name: a player appearing under two ids, with the same statistic, is enough to
produce it. For tournaments the name is the event title, which STX
can rename.

Map on `grouping_id`; show `grouping_name`.
:::

---

## Map a game in six steps

1. **Pull markets for the competitions you follow** with
   [`GET /api/v1/markets`](/api/rest/markets/list-markets/).

   ```
   GET /api/v1/markets?competitions=NCAAF&status=open&trading=true
   ```

   Results are ordered tradeable-first, and the response is paginated: follow
   `cursor` until it comes back `null`.

2. **Group by `event_id`.** Every market on one game shares it. `event_start`,
   `event_title` and `participants` on any of them describe the game.

3. **Group by `grouping_id`.** Each group is one set of mutually exclusive
   outcomes, one of your instruments. This works on an unfiltered bulk pull
   too: `grouping_id` carries the event id, so grouping a whole dump by it
   alone will not merge two fixtures' moneylines.

4. **Within a set, key each market on `(rules, specifier)`** and store its
   `market_id`. That pair is the leg: `rules` for winners and draws, the
   `home`/`away`/`both` prefix in `specifier` for stat lines, home-relative for
   handicaps, over for totals. `market_id` is what you send back when you trade.

5. **Read `max_price` per market** and convert prices into your own
   representation, minding the units table below.

6. **Subscribe for changes.** `GET /api/v1/markets` is a snapshot; the
   [`markets`](/websockets/channels/markets/) channel pushes only the fields
   that change afterwards. New markets on a game you already track arrive as
   `market_created`, so a live client can map them without another REST call.

:::note[Where the grouping fields appear]
`grouping_id` and `grouping_name` are returned by `GET /api/v1/markets` and on
every `market_created` push, so a market opened after your snapshot can be filed
into its book without another REST call. They are absent from `market_updated`
for the ordinary reason any field is absent from a diff: neither ever changes
for a market that already exists. Cache them per market on first sight.
:::

---

## Identifying a market

| Field | What it is | Use it for |
| --- | --- | --- |
| `market_id` | UUID, fixed for the life of the market | **The key in your own store.** Every order, cancel and channel topic takes it |
| `grouping_id` | Opaque string shared by every market in one set of mutually exclusive outcomes | **Lining our book up against your instrument.** Compare for equality; never parse |
| `event_id` | UUID shared by every market on the game | **Grouping a game's markets.** Needs no string handling |
| `rules` | What the market asks, machine-readable | Deciding how to interpret `specifier`, and which leg of a set this is |
| `specifier` | The strike: the line, the handicap, the stat | The threshold the result grades against |
| `grouping_name` | The set in words, e.g. `"Spread"`, `"AL East Division"` | Display and logs. **Not a key** |
| `symbol` | Readable name, e.g. `STXNCAAF-26SEP051530TULNDUKE-SPREADDUKEMINUS7.5` | Logs, dashboards, anything a person reads |

The mapping key is **`event_id` + `rules` + `specifier`**, which identifies
exactly one market. `grouping_id` sits above it and says which markets belong
together. Store `market_id` alongside whatever
you match on, and you can act on the market without resolving it again.

:::caution[`symbol` is a label, not an identifier]
`symbol` is rebuilt from the event whenever the event changes, and **the event's
start time is one of its inputs**. Every market symbol on a fixture moves when
that fixture is rescheduled.

```
Osasuna vs Celta Vigo, postponed eleven days
  before   STXLALIGA-26AUG161530OSACEL-DRAWOSACEL
  after    STXLALIGA-26AUG271430OSACEL-DRAWOSACEL
```

Nothing about the market changed. A stored symbol will not find it again.
`market_id` and `grouping_id` both survive a reschedule; `symbol` does not, so
do not key on it.

`title`, `description`, `max_price` and `featured` can also be edited after a
market opens. In rare cases a `specifier` is rewritten too, when the upstream
key for a participant changes.
:::

---

## Money on the wire

The same field carries different units depending on which surface you read it
from. This is the single most common source of mapping bugs.

| Field | REST `/api/v1/markets` | `markets` / `market_updates` channels |
| --- | --- | --- |
| `max_price` | `"1.0000"`, dollar string | `100`, cents |
| `last_traded_price` | `"0.5500"`, dollar string | `55`, cents |
| `price` | `"0.6000"`, dollar string | `60`, cents |
| `bids[].price`, `offers[].price` | `"0.5300"`, dollar string | `53`, cents |
| `recent_trades[].price` | `"0.5500"`, dollar string | `55`, cents |

Reading the same market both ways returns `"0.5300"` over REST and `53` on the
`markets` channel for the identical price level.

:::caution[The order book channel is a third format]
`market:<market_id>` is not the column above. Its `order_book_update` sends book levels
in **dollars as JSON numbers** (`0.53`), under abbreviated keys: `ob.b` and `ob.o`,
each level `{p, q, l, tc, tl}`. So one price level reads `"0.5300"` over REST, `53` on
`markets`, and `0.53` on `market:<market_id>`. Check which surface you are holding
before converting anything.
:::

:::note[Order prices use the REST format]
`POST /api/v1/orders` takes `price` as a dollar string with at most two decimal
places, so a REST book price such as `"0.5300"` can be sent as it is. A price
taken from the `markets` channel is in cents and has to be divided by 100 first.
:::

Two further details on the REST decimal strings:

- **Parse them as decimals.** Money has at least four decimal places and some
  fields carry more, so never compare them as strings.
- **`price_change24h` is a percentage**, rounded to a whole number, not a
  price delta.

---

## One market is one outcome

Every market is a contract on a single named outcome. It settles at the
market's `max_price` if that outcome happens and at `0` if it does not.
**Buying** means you expect it to happen; **selling** means you expect it not
to. You always trade against other participants, never against the exchange.

The outcome is named by two fields together: `rules` says what kind of question
the market asks, and `specifier` gives the strike. Everything else on the
payload (titles, questions, descriptions) is prose derived from those two.

:::caution[Settlement is not always all-or-nothing]
Two results settle between the extremes. `push` applies to a `home_winner` or
`away_winner` market whose game ends level: neither side won, so it settles at
`max_price / 2`. `settled` means the market was resolved at a price strictly
between `0` and `max_price`. Branch on `result` rather than assuming a winning
contract is always worth `max_price` and a losing one always `0`.
:::

### Which side a market takes

| Family | Sides on the exchange | One `grouping_id` covers |
| --- | --- | --- |
| Winner, 2-way | Two markets: `home_winner` and `away_winner` | Both |
| Winner, 3-way | Three markets: `home_winner_regulation_3way`, `away_winner_regulation_3way`, `draw_regulation_3way` | All three |
| Totals | **One** market per line, and it is the **over**. Sell it to be short the over | The whole ladder |
| Handicap | One market per line, always stated **from the home team's side** | The whole ladder |
| Team/event stat | One market per stat, and per team where the stat is team-scoped | One stat on one side |

To pair `home_winner` with `away_winner`, match on `grouping_id`: both sides of
a moneyline carry the same value, and so do all three legs of a three-way and
every line of a ladder.

:::note[Two sides of one question can be two sets]
For `event_stat_line`, a set is one statistic on one side. `"home|FIRST_SCORE|NA"`
and `"away|FIRST_SCORE|NA"` therefore have **different** `grouping_id` values,
even though at a glance they look like the two halves of "who scores first".
They are independent yes/no contracts and are priced as such. If you model that
question as a single two-way instrument, pair the two sets yourself on the
statistic.
:::

---

## `rules`: what the market asks

These are the values this page covers. Scoped variants are listed separately
under [Scope](#scope-markets-on-part-of-a-game).

| `rules` | Question | Event types |
| --- | --- | --- |
| `home_winner` | Does the home team win? | baseball, basketball, football, hockey, soccer, cricket |
| `away_winner` | Does the away team win? | baseball, basketball, football, hockey, soccer, cricket |
| `home_winner_regulation_3way` | Home win in regulation, draw excluded | soccer, cricket |
| `away_winner_regulation_3way` | Away win in regulation, draw excluded | soccer, cricket |
| `draw_regulation_3way` | Does the game end level in regulation? | baseball, soccer, cricket |
| `home_winner_regulation_2way` | Home win in regulation, 2-way | soccer, cricket |
| `away_winner_regulation_2way` | Away win in regulation, 2-way | soccer, cricket |
| `home_winner_regulation` | Home win in regulation | cricket |
| `away_winner_regulation` | Away win in regulation | cricket |
| `over_under` | Do both teams combine for more than the line? | baseball, basketball, football, hockey, soccer, cricket, tennis |
| `spread` | Does the home team beat the handicap? | baseball, basketball, football, hockey, soccer, tennis |
| `event_stat_line` | A game-level or team-level stat question | baseball, basketball, football, hockey, soccer |
| `participant_stat_line` | A team total for one named stat | baseball, basketball, football, hockey, soccer |

The event types listed are the ones each rule is defined for. Which of those
markets actually open is a per-environment choice, so a sport can support a rule
without any live markets in it at a given moment.

:::caution[`rules` is not unique across sports]
`over_under` on a baseball game and `over_under` on a soccer game are graded by
different logic, and their period units differ. Always read `rules` together
with `event_type`.

New rules appear from time to time. Match the values you recognize and fall
through gracefully on one you do not, rather than treating this as a closed set.
The same holds for `grouping_id`: treat it as opaque, so a family you have not
seen before still groups correctly.
:::

---

## `specifier`: the strike

| `rules` | `specifier` format | Example |
| --- | --- | --- |
| All winner and draw rules | `null` | `null` |
| `over_under` | A half number, the line | `"54.5"` |
| `spread` | A signed half number, **from the home team's side** | `"-7.5"` |
| `participant_stat_line` | `{home\|away}\|{STAT}\|{line}` | `"home\|POINTS\|57.5"` |
| `event_stat_line` | `{home\|away\|both}\|{STAT}\|{line}` | `"both\|HITS\|16.5"`, `"home\|FIRST_SCORE\|NA"` |

Lines are always half numbers, so a totals or handicap market cannot end level
and there are no pushes on them.

### Handicaps are stated from the home side

`spread` with `"-7.5"` is *home team wins by more than 7.5*; with `"7.5"` it is
*home team wins, or loses by less than 7.5*. The away team never gets its own
handicap market; to be on the away side of the line, sell the home market.
Every line of the ladder shares one `grouping_id`.

### Stat lines: three fields, always

Split a stat-line specifier on `|`. It always has three fields:

1. the side: `home`, `away`, or `both` for a statistic about the game itself
2. the statistic code
3. the line, or `NA` where the statistic states no threshold

For countable stats the line is a real over/under threshold:

```
"both|HITS|16.5"      Will both teams combine for more than 16.5 hits?
"both|ANY_SCORE|39.5" Will either team score 40+ points?
"home|POINTS|57.5"    Will the home team have more than 57.5 total points?
```

For yes/no stats there is no threshold, and the line field reads `NA`:

```
"both|OVERTIME|NA"                Will the game go to overtime?
"home|FIRST_SCORE|NA"             Will the home team score first?
"both|FIRST_SCORE_TOUCHDOWN|NA"   Will the first score be a touchdown?
```

`NA` is the signal, so you do not need to know which statistics are countable
to render a market correctly: a numeric third field means an over/under, and
`NA` means a proposition. Fall back to the market's `question` for display.

The set of stat codes in use is a per-environment setting rather than a fixed
part of the API, so treat any list you build as the set in use today and handle
an unrecognized code by skipping the market rather than failing.

---

## Scope: markets on part of a game

A market that settles over part of a game carries a scope suffix on `rules`.
No suffix means the full game, which is most markets.

| Suffix | Window |
| --- | --- |
| *(none)* | Full game |
| `_f2`, `_f5` | The first 2 or 5 periods |
| `_r1`, `_r2` | Period 1, period 2 |

```
over_under        full game
over_under_r1     first period only
home_winner_f5    first five periods
spread_f2         first two periods
```

The scope is also a segment of `grouping_id`, so two scopes of one rule never
share a set:

```
{event}:total:full    over_under
{event}:total:r1      over_under_r1
```

:::caution[A scope code is not a fixed duration]
The period *unit* depends on the sport: `_r1` is the first inning in baseball
and the first half in soccer. Read the suffix against `event_type`, never on
its own, and note that `grouping_id` carries the raw code (`r1`), not the
period, so it does not settle this for you either.

`grouping_name` does spell the period out (`"Spread - 1st Inning"` against
`"Spread - 1st Quarter"`), which is useful for display, but it is not a key.
:::

Two scopes of one rule are two independent markets with separate books.
`over_under` and `over_under_r1` on the same game are unrelated instruments.

---

## Naming the side: which text to trust

Every market carries several human-readable strings, and they are not
interchangeable.

| Field | For a `home_winner` market |
| --- | --- |
| `title` | `"NCAAF - Week 1 TULN @ DUKE"` |
| `short_title` | `"TULN @ DUKE"` |
| `group_title` | `"Duke"` |
| `position` | `"Duke Blue Devils"` |
| `grouping_name` | `"Moneyline"` |
| `question` | `"Will the Duke Blue Devils defeat the Tulane Green Wave?"` |
| `description` | `"Contracts for this market settle into $1 if the Duke Blue Devils beats the Tulane Green Wave and settle into $0 if they do not."` |

:::caution[`title` and `short_title` do not identify a winner market]
On `home_winner` and `away_winner` markets for the same game, `title` and
`short_title` are **identical**: both describe the fixture, not the side. The
away market above also reads `"TULN @ DUKE"`. Keying a winner market on either
field silently merges the two sides of a moneyline into one record.

`group_title` and `position` name the side. `description` is the definitive
statement of what settles the contract. `grouping_name` names the *set*, so it
is identical on both sides by design; it is the one string on this list that
is meant to be shared.
:::

Handicap and totals markets do include the line in `short_title`
(`"TULN @ DUKE -7.5"`, `"BC @ CIN OU 54.5"`), which is why the problem is easy
to miss until a moneyline reaches your book. `position` is also unreliable on
stat-line markets, where it can be the bare stat code (`"FIRST_SCORE"`); for
those, `grouping_name` reads as the statistic and its side: `"Last Run (BOS)"`.

### `participants` describes the fixture, not the market

`participants` is the game's two teams (`role` of `away` and `home`, away
first), and it is **the same on every market of that game**. It does not tell
you which side a market settles on. Read that from `rules` for winner markets,
and from the side prefix in `specifier` for stat lines.

```json
"participants": [
  {"name": "Tulane Green Wave", "role": "away", "short_name": "Green Wave", "abbreviation": "TULN"},
  {"name": "Duke Blue Devils",  "role": "home", "short_name": "Blue Devils", "abbreviation": "DUKE"}
]
```

---

## `max_price` is a per-market field

`max_price` is the settlement value of one winning contract, in dollars, and the
ceiling on order prices. An order must price **strictly below** it.

Read it from each market. It is not a per-region constant: a single environment
can carry markets at several different values at the same time, and one
competition's markets can differ from another's. A client that hardcodes a
value will have orders rejected, or will misprice settlement value and
sell-side liability.

```json
{"symbol": "STXNCAAF-26SEP051530TULNDUKE-GAMEDUKE", "max_price": "1.0000"}
```

`max_price` also sets the reference point for sell-order liability; see
[Understanding positions](/concepts/positions/).

---

## The order book on a market

`bids` and `offers` on a market carry the **top seven price levels** per side,
each a `{price, quantity}` object, with quantity aggregated across all resting
orders at that price. They are a summary for display and mapping, not a feed to
trade from: for the full aggregated book on one market, join the
[order book channel](/websockets/order-book/).

**Both arrays are sorted by price descending.** For bids that puts the best
price first; for offers it puts the best price **last**.

```json
"bids": [
  {"price": "0.4900", "quantity": "120.00"},
  {"price": "0.4700", "quantity": "40.00"},
  {"price": "0.4500", "quantity": "75.00"}
],
"offers": [
  {"price": "0.5700", "quantity": "60.00"},
  {"price": "0.5400", "quantity": "25.00"},
  {"price": "0.5100", "quantity": "90.00"}
]
```

Best bid is `bids[0]` at `0.49`; best offer is `offers[len - 1]` at `0.51`.
Taking `offers[0]` gives you the *worst* offer in the summary, up to seven levels
away from the touch: a mistake that reads as a wide spread rather than as an
error.

Best bid and best offer are enough to compute the implied probability of the
outcome: a market with a best bid of `0.49` and a best offer of `0.51` on a
`max_price` of `"1.0000"` is trading around a 50% chance.

Each market in a set has its own book. A set is a set of related contracts, not
a combined book.

---

## What this page does not cover

You will see these markets in the API. Identify them by `rules` and handle them
deliberately rather than letting them fall into your game-market mapping.

| `rules` | What it is |
| --- | --- |
| `player_stat_line` | Player props. `specifier` is `{player}\|{player_id}\|{STAT}\|{line}`: four fields, and the player's team is not in the payload |
| `division_winner`, `conference_winner`, `superbowl_champion`, and similar | Season futures. `specifier` is a team abbreviation, and the market is not tied to a single game |
| `race_winner` | A field of participants rather than a fixture |
| `ad_hoc_rule` | Hand-created markets with no machine-readable settlement definition; `description` is the only statement of what settles |
| `combo_rule` | Combination markets built from other markets' legs; `participants` is empty |

The identity rules on this page hold for all of them: `market_id` is the key,
`event_id` groups a game, `grouping_id` groups a set, and `rules` plus
`specifier` name the outcome. Only the `specifier` grammar and the settlement
source differ.

`grouping_id` matters most on the season futures, which is the one shape where
`event_id` is not enough. A single NFL season event carries 96 markets in 11
sets (the Super Bowl, both conferences and all eight divisions), and grouping
that event by `event_id` pools them into one. `grouping_name` reads as the set:
`"AFC South Division"`, `"American League"`, `"World Series"`.

```json
{
  "rules":         "division_winner",
  "specifier":     "JAX",
  "grouping_id":   "201e3810-064d-4ff3-8246-5b9be6dfef0f:division_winner:full:AFCSOUTHDIVISION",
  "grouping_name": "AFC South Division"
}
```
