closing-lines
One row per (game, operator, market, selection, side): where the side opened, where it closed, how much it moved, and book-only CLV. Built from change-only odds records observed from scrape start through game end. One row per priced selection id per side — ≈ 12,000 rows for an MLB game with full props coverage — the compact, analysis-ready summary of a game’s odds; reach for sportsbook-lines when you need every intermediate move.
Fetching
Section titled “Fetching”curl -s "https://api.betflux.ai/v1/games/NBA_ORL_DET_20260503/closing-lines" \ -H "Authorization: Bearer $BETFLUX_API_KEY" -o closing.parquetOne Parquet file per game. Date-range queries are a client-side loop over
/v1/games — the CLI and SDK do it
for you, with local filters (operator, market_type, team, player_id,
side):
betflux get closing-lines --league NBA \ --date-from 2026-05-01 --date-to 2026-05-07 --operator FANDUELSemantics
Section titled “Semantics”- One row per selection id per side:
market_key(=OPERATOR:market_external_id) andselection_external_ididentify the market and selection by the operator’s own ids, so two markets or two players that share a name never share a row — see Market identity. - Books that mint a new selection id per line value (DraftKings, BetMGM,
Pinnacle, FanDuel alternates) show a moved line as a new row whose opening
equals its closing. To get DraftKings’ main-line close for a side, take the
row with the latest
closing_tsamong the market’s rows for that side — a heuristic until a line-stable key ships. FanDuel main markets keep one id, so that row’s opening → closing is the whole pre-game move. - Opening = the side’s quote when its league’s market goes live (see
Opening cutoff). Closing = the latest
eligible quote strictly before
game_start: both the market and selection must be explicitly active, prices must be valid, and a recognized market must have its required selection values and compatible sides. Its observed market meaning must also agree with the market’s type at close; an earlier points quote cannot become a rebounds quote or another player’s quote through relabeling. Missing or unknown status does not establish an active quote. - A closing quote’s price, applicable spread/total, and timestamp come from the same observation. A later suspended or incomplete observation does not replace it, and an old threshold is never combined with a newer price.
- From
dataset_versionv6, a selection first observed at or aftergame_start(an in-play-only market) has no row: it can never have a closing line. Find those in market-results or sportsbook-lines. Games still served at v5 include them with null closing columns. - From
dataset_versionv8, a closing quote also needs its book to have been observed for the game, in any market, in the 24 hours beforegame_start. A book we stopped receiving odds from has no close: its last quote could be days or months old. A market whose price simply stayed put still closes, because the rest of the book kept updating. Games still served at v7 carry that stale quote in the closing columns, mostly NFL before November 2025. - From
dataset_versionv10, darkness is judged per operator event: when a book deleted its event for the game and relisted it under a new one, the old event’s selections have no close, while the new event’s close as usual. Rows without an operator event id are judged across the whole book, as before. - If no eligible pre-game quote exists, all closing columns are null, including for all-suspended histories. The row, opening observation, and history statistics remain present.
- Odds come in three encodings per snapshot: American (
*_odds), decimal (*_decimal), and implied probability (*_implied, vig included). line_move_countcounts odds changes between consecutive records;first_seen_at/last_seen_atandmin_odds/max_odds/distinct_odds_countspan all records including live ones.scopeis reserved and always null.
Opening cutoff
Section titled “Opening cutoff”Books post look-ahead prices weeks before a market is really trading, often
just to show the game, and then reprice when it goes live. From
dataset_version v10 the opening is taken at a per-league cutoff measured
from when the books actually reprice (adr-0096), so those early prices don’t
feed opening_* or CLV:
| League | Opening cutoff (opening_cutoff_ts) |
|---|---|
| NFL | Monday 08:00 ET after the previous week’s Sunday. A Thursday game belongs to the coming Sunday, a Monday game to the Sunday before it. |
| NCAAF | Sunday 12:00 ET after the previous Saturday. Weekday games belong to the coming Saturday; a Monday game to the Saturday before it. |
| Other leagues | 48 hours before game_start. |
opening_method says which quote opens the row:
AS_OF_CUTOFF: the book was being captured for this game in the 24 hours before the cutoff, so the opening is the quote it was showing at the cutoff.opening_tscan be earlier than the cutoff when the book held that price.FIRST_AFTER_CUTOFF: the book was not being captured then, so its last quote at the cutoff may be a look-ahead price. The opening is its first quote after the cutoff and beforegame_start.- Null: neither exists, so the opening columns and CLV are null.
first_quote_odds, first_quote_implied, first_quote_spread and
first_quote_total keep the first quote ever seen, which is what
opening_* meant before v10. first_seen_at is still its time. On games
built before v10 the new columns are null and opening_* is the first quote.
CLV (closing line value)
Section titled “CLV (closing line value)”Book-only CLV compares a side’s own open to its own close at the same book:
clv_odds=closing_odds − opening_oddsin American odds cents, counting −100 and +100 as the same price: −102 → +100 is 2, −110 → +110 is 20clv_implied=opening_implied − closing_implied(implied-probability delta; implied probabilities include vig)
Both are null when either price is unavailable, or when the opening and closing observations represent incompatible market meanings or selection identities — different periods or metrics, or a relabeled selection.
CLV uses the opening above, so from v10 it measures the move from the live market’s open, not from a look-ahead price.
Before dataset_version v9, clv_odds subtracts the raw American odds, so
a price that crossed even money reads 200 too far from zero (−102 → +100
shows 202). Moves that stay on one side of even money are the same in every
version.
For cross-book fair-value work, sportsbook-lines carries devigged fair-value columns computed by six de-vig methods.
Columns
Section titled “Columns”| Column | Type | Nullable | Description |
|---|---|---|---|
game_id | string | no | Game id: LEAGUE_AWAY_HOME_YYYYMMDD (ET date; `_2` suffix for the second game of a doubleheader). |
league | string | no | League code, e.g. NBA, NFL, MLB, NHL, NCAAM, NCAAF. |
game_date | string | no | Calendar date of the scheduled start in US Eastern time (YYYY-MM-DD). |
game_start | string | no | Scheduled start time, ISO-8601 UTC. |
home_team | string | no | Canonical home team name. |
away_team | string | no | Canonical away team name. |
operator | string | no | Sportsbook operator code, e.g. FANDUEL, DRAFTKINGS, PINNACLE. |
market_type | string | no | Market type, e.g. SPREAD, MONEYLINE, TOTAL, PLAYER_METRIC_OVER_UNDER. A type, not an identity: a book can offer several markets of one type (main + alternate lines, one per player). |
period | string | no | Game segment the market covers, e.g. the full game, a half, or a quarter. |
scope | string | yes | Reserved for future market scoping; always null. |
market_key | string | no | Global market identity: `OPERATOR:market_external_id` — the operator code plus the operator's own market id. Join key across closing-lines, market-results and sportsbook-lines. |
selection_name | string | yes | Operator's label for the selection (e.g. a player for prop markets, `Over`); display only — shared across selections, never an identity. Null for plain team markets. |
side | string | no | Priced side within the selection, e.g. HOME, AWAY, OVER, UNDER. |
opening_odds | integer | yes | American odds of the opening quote: the quote at the league's opening cutoff when the book was being captured then, else the first quote after the cutoff and before game_start; null when neither exists. See `opening_method` and adr-0096. The first quote ever seen is `first_quote_odds`. |
opening_decimal | number | yes | Decimal odds at open. |
opening_implied | number | yes | Implied probability at open (vig included, 0-1). |
opening_spread | number | yes | Point spread (handicap) at open; null for non-spread markets. |
opening_total | number | yes | Total (over/under line) at open; null for non-total markets. |
opening_ts | string | yes | Timestamp of the opening quote, ISO-8601 UTC; it can be before `opening_cutoff_ts` when the book held that quote through the cutoff. |
closing_odds | integer | yes | American odds of the last eligible active quote strictly before game_start; null when no eligible pre-game quote exists. |
closing_decimal | number | yes | Decimal odds at close. |
closing_implied | number | yes | Implied probability at close (vig included, 0-1). |
closing_spread | number | yes | Point spread (handicap) at close. |
closing_total | number | yes | Total (over/under line) at close. |
closing_ts | string | yes | Timestamp of the last eligible active pre-game quote, ISO-8601 UTC; null when no eligible pre-game quote exists. |
min_odds | integer | yes | Lowest American odds observed across all records, pre-game and live. |
max_odds | integer | yes | Highest American odds observed across all records, pre-game and live. |
distinct_odds_count | integer | yes | Number of distinct American-odds values observed. |
line_move_count | integer | yes | Number of records where the odds changed from the previous record. |
pre_game_records | integer | yes | Change-only records observed before game_start. |
live_records | integer | yes | Change-only records observed at or after game_start. |
first_seen_at | string | yes | Timestamp this side was first observed, ISO-8601 UTC. |
last_seen_at | string | yes | Timestamp this side was last observed (including live), ISO-8601 UTC. |
clv_odds | integer | yes | closing_odds - opening_odds in American odds cents, counting -100 and +100 as the same price (-102 to +100 is 2); null when either is missing or the opening and closing market meanings are incompatible. |
clv_implied | number | yes | opening_implied - closing_implied; null when either is missing or the opening and closing market meanings are incompatible. |
market_player_ids | string[] | yes | Player UUIDs referenced by the market, as of the closing record. |
selection_player_ids | string[] | yes | Player UUIDs referenced by the selection, as of the closing record. |
market_metric | string[] | yes | Structured stat metric(s) the market prices (SportsMetric enum, e.g. FOOTBALL_RECEIVING_YARDS); null for non-player-metric markets. Join key for prop analysis — no name parsing needed (adr-0027). |
selection_metric | string | yes | Structured metric for the priced selection; set only for method-of-score selections, else null. |
market_external_id | string | no | Operator's market id — the identity of the market within (game, operator); `market_key` is `operator` + `:` + this value. Same value as `market_external_id` on market-results and sportsbook-lines. |
market_name | string | yes | Operator's market label, e.g. `Run Line Alternate`; display only. |
selection_external_id | string | no | Operator's selection id — the row identity together with `market_key` and `side`. For FanDuel main markets it is stable and the line moves on it (one row, opening → closing). DraftKings, BetMGM, Pinnacle and FanDuel alternates mint a new id per line value, so one market carries several rows per side and a moved line is a new row with opening == closing. |
is_mainline | boolean | yes | True when the selection belongs to the operator's primary (non-alternate) full-game spread, moneyline or total as of the closing quote — the headline line, not a rung of the alternate ladder. Period lines, team totals and props are false. Null on games built before v7. |
opening_method | string | yes | How the opening quote was chosen: AS_OF_CUTOFF (the quote the book was showing at `opening_cutoff_ts`, the book being captured for this event in the 24 hours before it) or FIRST_AFTER_CUTOFF (the first quote after the cutoff and before game_start). Null when there is no opening, and on games built before v10. |
opening_cutoff_ts | string | yes | The league's opening cutoff for this game, ISO-8601 UTC: Monday 08:00 ET after the previous NFL Sunday, Sunday 12:00 ET after the previous NCAAF Saturday, else 48 hours before game_start (adr-0096). Null on games built before v10. |
first_quote_odds | integer | yes | American odds of the first quote ever seen for this side, which may be a look-ahead price posted weeks before the market was live. Before v10 this was `opening_odds`. Null on games built before v10. |
first_quote_implied | number | yes | Implied probability of the first quote ever seen (vig included, 0-1). Null on games built before v10. |
first_quote_spread | number | yes | Point spread of the first quote ever seen; null for non-spread markets and on games built before v10. |
first_quote_total | number | yes | Total of the first quote ever seen; null for non-total markets and on games built before v10. |
dataset_version | string | yes | Version of the dataset this game's file was built at, e.g. `v5`. Games in one pull can differ: versions of a compatibility line only add columns or carry documented changes (see `versions` on /v1/datasets). Columns added after a game's version read null for it. Files built before this column existed lack it entirely (null when read with DuckDB `union_by_name = true`); the `X-Betflux-Dataset-Version` response header still names their version. |