Skip to content

Sportsbook and special-event markets: the schema (draft, 2026-10-06)

Owner decisions (2026-10-06): sportsbook analysis stays separate from the web app, CLI tools only; events are defined by exchange-agnostic schemas; the deployment target is the hosted MCP server (docs/mcp.md), not the web app. The full roadmap (coverage of the three generic sportsbooks (A: decimal odds, B and C: American odds) boards, tiers per market, phases) lives in the HL project library; this page is the part the repo needs: the schema, the vocabulary, the CLI and the MCP surface.

The owner's rule: sportsbook analysis is CLI only, outside the app and its database for now, and events are defined by schemas that no venue owns. The repo already defines sports (sports/<code>.toml), exchanges (exchanges/<code>.toml, read by racinglines/exchanges.py) and input frames (racinglines/frames/schema.py) as data with a validator. The proposal adds three more files of the same kind. Draft examples, built from real board rows, are under schemas/ and load through racinglines/books/schema.py (tests/test_books_schema.py loads every one).

File One per Holds Example
events/<sport>/<season>-<round>-<slug>.toml event sport, season, round, the launch-spec key (live/f1/2026-17.toml uses event = "2026-17"), race_id (the DB's exact key), format, session start times (UTC), the market kinds the event opens and their lines. No venue, no prices, no entrants (entrants come from the DB entry list for race_id) events-f1-2026-17-singapore.toml
venues/<code>.toml venue code, name, kind = "sportsbook" \| "exchange", odds format, currency, who settles, observed caps and voids as notes, and [[rules]]: regex on the venue's market title → kind + subject, exactly the shape of exchanges/og.toml [[sports.f1.rules]]. Selections resolve to the entry list through an exact alias table ("Sainz Jr, Carlos" → "Carlos Sainz"), never fuzzy; an unknown name is unmapped venues-book_a.toml
books/<venue>/<date>-<event>.toml capture venue, event key, capture time (UTC), source (screenshot / paste / api) and its reference, odds format, and [[lines]]: the venue's own title and selection untouched, the odds, a transcription note, and the market key the rules produced, written into the file so the mapping is reviewable before pricing; optionally [book] image (the screenshot) and a line's box = [x, y, w, h], the pixels on the screenshot where book render --style hud puts that line's label (render) books-book_a-2026-10-05-singapore.toml
(same) capture A second venue in the same shape, American odds, same event file untouched: sportsbook B's 22-driver winner board from the owner's screenshot (2026-10-05 22:59 PDT). Its overround is 23.3%; the model comparison is the "track sportsbook bets" thread's job books-book_b-2026-10-05-singapore.toml

Odds presentations (venue.odds and book.odds; racinglines/books/schema.py checks each, to_prob reads a quote as an implied probability with the venue's margin still in it, present writes a fair value in the venue's own units). The names are provisional: the owner checks them after the merge (todo: owner decisions).

Name Looks like Where Means
decimal 1.85 European sportsbooks (sportsbook A) payout per unit staked, stake included
american -118 / +150 US sportsbooks (sportsbooks B and C) stake to win 100, or the win on a 100 stake
fractional "7/4" UK sportsbooks the win per unit staked, as a string
cents 37 Kalshi price of a $1 YES contract, in cents (1 to 99)
dollars 0.37 Polymarket, OG.com price of a $1 share, in dollars (0 to 1)
prob 0.37 our fair values a plain probability

What makes it exchange-agnostic. The market key is (kind, subject, params) in the kinds registry's vocabulary (racinglines/markets/kinds.py), the same key Kalshi's and Polymarket's classifiers already produce and OG.com's rules file produces. A sportsbook A line, an sportsbook C American-odds line and a Kalshi contract for the same thing carry the same key, so one fair value prices all three and one settle() decides them. A new venue is a venues/<code>.toml; a new market type is a kind (with fair and settle) and one rule line per venue that lists it. Exchanges' own API schemas stay in exchanges/; a venues/ file for an exchange would hold only its title rules, which for OG.com already live in exchanges/og.toml (one of the two should own them: decision).

The CLI around it (all read-only against the app; nothing writes to market_links or any app table). book map, book price and book settle are built (2026-10-07), and book export --ledger writes a book's staked lines as ledger rows (2026-10-11, Ledger export): Sportsbook slips has their actual interface, the EV definitions and the JSON fields. book render (2026-10-09, Bet-slip graphics) draws a priced board as a HUD on the book's screenshot, a card or a book-agnostic graphic, with recommended stakes from the v2 rule in markets/books.toml; book new --from-text drafts a book file from pasted lines. The table below is the original proposal; shrink, picks CSV, writing back into the book file and updating a ledger in place are not built:

Command Does
racinglines book map <book.toml> applies the venue rules, writes market keys into the book file, lists every unmapped line and every unknown selection name
racinglines book price <book.toml> --run <id> fair value per mapped line from a stored run's predictions (MCP get_predictions on the VM, or a local run id), no-vig, shrink, EV, Kelly at the configured fraction and bankroll, picks CSV; the output keeps the run id, commit and latest race in the data on every row
racinglines book settle <book.toml> kinds.settle() from the classification once the race is in the DB; writes result and payout per line, and updates ledger.csv rows that reference the book
racinglines event new <sport> <season> <round> writes the event file from the DB (race_id, sessions, format) so the exact key is never typed by hand

Settings for the shrink (halfway to the book's no-vig price), the Kelly fraction (the owner uses half; the cycling CLI defaults to a quarter, racinglines/cli/cycling.py:145-146), bankroll and the longshot filter live in one TOML with a decision-log entry. Sportsbook lines stay out of market_links, the disagreement log and the app's CLV work until the owner says otherwise; CLV for hand-placed bets is computed by the CLI from the book's own later captures (or an exchange's close as a proxy, docs/todo.md:279) into the ledger's closing_odds column.

Validation (built, second commit). racinglines/books/schema.py in the style of frames/schema.py: load_event, load_venue, load_book and load (picks the schema from the top-level table) return the parsed dict or raise BookError listing every problem with its field: required sections and fields, odds that can be odds in the file's format (decimal > 1, American an integer of at least 100 either way, prob in (0, 1)), UTC times (ISO 8601 ending in Z, or "unknown" for a session), every market.kind in kinds.KINDS or props.PROP_KINDS, every rule title a compilable regex, every line either "unmapped" or a market table carrying the driver / team / opponent / line its payoff needs. tests/test_books_schema.py loads every example under schemas/ (as tests/test_exchange_schema.py does for exchanges/, docs/exchanges.md:26). The five classification kinds the vocabulary needed are in the registry with fair and settle (kinds.py, default=False, so the app's summaries and the golden tests are unchanged).

Adding a market type (owner, 2026-10-06: "as few conditional code switches in the model, and more generalized support functions who's inputs are set by the schemas"). The classification and retirement kinds are declarative: one [[kinds]] table each in markets/kinds.toml with a payoff spec (subject driver / team / field, a predicate on one car, an aggregate for a team or the field, compare = "over" for a line), priced and settled by the two generic functions of racinglines/markets/payoffs.py. A new market type is a new table there plus a venue rule line; code changes only when it needs a predicate the table doesn't have yet. The legacy kinds (win, podium, pole, head-to-head, top constructor, standings, ...) keep their code in kinds.py until a later migration under the promotion rule, since their fair values feed the golden tests and the live book.

Combos (same-game parlays, 2026-10-07). A slip like "Race Winner and Fastest Lap" is one line whose market is { kind = "combo", legs = [ { kind = "race_win", driver = "..." }, { kind = "race_fastest_lap", driver = "..." } ], void_leg = "void_all" }: two or more legs, each a market table of a single kind (no combo inside a combo), and the book's rule for a void leg ("void_all", the default: a void leg voids the slip; "drop_leg": the other legs decide). racinglines/markets/combos.py prices it as the share of simulations in which every leg holds, from the same simulations as the single markets (combo_fair; price adds each leg's marginal, their product and the lift), so legs that move together price above the product. Every kind decided per simulation can be a leg: win / podium / top n, the declarative kinds through their spec predicates, pole and the sprint stages, head-to-head, top constructor (a points tie splits it), biggest mover, and the fastest lap when the run draws it (variant fastlap or flpos). A season standings market or a history-rate prop (safety car, red flag, rain) can't be a leg: pricing raises an error naming it. Settlement takes each leg's own rule; any losing leg settles the combo NO, and a fastest-lap leg otherwise returns None (a manual settlement: the classification records no lap times). On the VM the Lab job f1_combo prices combos for one event (MCP run_job, then get_model_run; docs/mcp.md).

What the exchanges list today, for the rules files. Polymarket's safety car / red flag / rain questions are linked but unpriced (docs/f1.md:178-181); Kalshi sprint markets price from the stand-in; Kalshi retirements and "race occurrence" are unmodeled (kalshi/sync.py:25-28); OG.com lists only champions and Race Winner (docs/exchanges.md:34-40). Polymarket has listed no F1 race since 28 Aug 2026 (docs/coverage.md:10), so near-term exchange coverage means Kalshi and OG.com.

MCP surface (the deployment target)

The CLI commands above are the same functions the hosted MCP server exposes, so a thread can price a board from the VM's current run without a cloud rebuild (owner, 2026-10-06: "the eventual deployment target is to add these sportsbook/special event analytics to the MCP tools"). Proposed tools, beside the existing list_markets, get_predictions and run_job (racinglines/mcp/):

Tool Does
price_book(book_toml, run_id=None) the book price command on a book file passed as text: mapped lines with fair, no-vig, EV and stake; unmapped lines listed
map_book(venue, lines) applies a venue's rules to raw (title, selection, odds) rows and returns the market keys, so a thread can transcribe a screenshot and get a reviewable mapping back
settle_book(book_toml) results per line from the classification once the race is in
list_kinds() the vocabulary: every kind, its status, subjects and known venue wordings (vocabulary.md)

The sportsbook files themselves are not stored in the app's database: a book is passed in and the priced table comes back, and the owner's ledger stays a file outside the app.

Vocabulary

vocabulary.md, generated from schemas/kinds-vocabulary.toml: 49 kinds, 29 of them in the registry (which holds 30: 17 before this branch, 13 declared in markets/kinds.toml on it: classified, last classified, both cars classified, at least one car classified, both cars in the points, number classified, winning constructor, retire, number of retirements, first, second and third retirement, first retiring constructor), 3 in props.py only, 16 proposed, 1 with no data source; 17 seen on two or more venues, 20 seen only on sportsbook A.