Skip to content

betting_combat.trading.rounds

Strategy 4 (rounds) inside the card run: the live side of rounds.live.

prepare (once, at the start of the run) the card's bouts linked to its Kalshi fights by
the fighters' names; the Dataset 1 rows of every scheduled round start
(``rounds.live.upcoming``) with the Kalshi card-start quotes as the pre-fight
price; layer 1 on them. A fight that cannot be linked or priced is not traded.
observe before each fight: its winner books (the ``mid_a`` of the round-0 row time,
which every later row's ``chg_since_pre`` reads)
decide at each round break — the fight clock's 'break' decision point, the first poll
that shows round k+1 (``domain.fight``: the feed switches when the round
starts) — and, with ``trade_round0``, when the feed first shows the fight live:
the trades before now -> Set 3 -> layer 2 -> candidates -> meta -> size ->
caps -> IOC orders at the quote + the cushion (``rounds.live.decision``)

The clock is the card run’s own (trading.tracker / domain.fight): the round anchors are the moments the feed first showed each round, never the research’s clock (derived from the result). The window edges are the feed’s start + 6k minutes.

Money: the card’s state is rebuilt from the run’s own orders and settlements at every decision (what this strategy staked per fight and on the card, what is still at risk, the settled P&L), so a relaunch resumes with the same caps. Every portfolio gate applies (kill switch, daily loss, drawdown), plus the strategy’s own daily stop on SETTLED P&L.

Data the run needs from outside (RoundsData): the UFC tables as of the card and the card’s bouts before it starts. Without it (or without an active model version) the strategy records why and does not trade.

Classes

A Kalshi fight and its UFCStats bout: swap when Kalshi’s fighter A (the first ticker) is the dataset’s fighter B (UFCStats slot a_slot decides who is A).

FightPlan

KalshiRoundsData

RoundsMarketData on the run’s Kalshi desk (trades: the WebSocket mirror, else REST) and REST (open interest).

open_interest

open_interest(market_id: str) -> float | None

prints

prints(market_id: str, since: dt.datetime, until: dt.datetime) -> list[Print] | None

Moment

One decision point of one fight.

MoneyLimits

Sam’s money controls in dollars (resolve_money).

Planned

What an order was sized on, to reprice it at send time (RoundsRunner.finalize).

RoundsData

Bases: Protocol

What the data layer supplies for a card run (SessionDeps.rounds_data).

history

history(card_date: dt.date) -> UfcTables

Every UFC table as the research read it, holding nothing dated on or after card_date (services.rounds_inputs.ufc_tables).

upcoming

upcoming(card_date: dt.date) -> UpcomingCard | None

The card’s bouts as UFCStats lists them before it starts (no results); the Kalshi quotes may be left out (the run takes them from the live books).

RoundsMarketData

Bases: Protocol

The Kalshi reads a decision needs beyond the books (trading.desks.KalshiDesk).

open_interest

open_interest(market_id: str) -> float | None

prints

prints(market_id: str, since: dt.datetime, until: dt.datetime) -> list[Print] | None

RoundsRunner

Strategy 4 for one card run.

card_state

card_state(orders: Mapping[str, OrderRecord], settlements: Mapping[str, SettlementRecord]) -> CardState

This strategy’s money on the card in its mode now, from its orders (filled cost; an order whose fate is unknown counts at its limit) and their settlements, in the order they settled. Paper and live never mix.

decide

decide(m: Moment, quotes: Mapping[tuple[str, str], Quote], market: RoundsMarketData, portfolio: PortfolioConfig, orders: Mapping[str, OrderRecord], settlements: Mapping[str, SettlementRecord]) -> tuple[DecisionRecord, list[OrderIntent]]

One decision: its record and its orders (none in shadow mode or on a no-trade).

finalize

finalize(record: DecisionRecord, intents: Sequence[OrderIntent], quotes: Mapping[tuple[str, str], Quote], m: Moment, sent_at: dt.datetime, stale_book_seconds: float, cash: float | None = math.inf) -> tuple[DecisionRecord, list[OrderIntent]]

At send time: every order repriced on the book read NOW (quotes), never on the decision’s quote. A bet whose side has changed or whose edge (after the fee) is gone, whose book is missing, stale or already priced (outside the 3-97c band), or which no longer affords one contract of each leg inside its reserve, is dropped; the others go at the fresh quote + the cushion, contracts cut so the reserve still covers them. cash: live, the Kalshi cash available now (None: unreadable, nothing is sent); the orders’ most they can cost (at their limits, fees in) never pass it, in decision order (paper: no cash cap). The record gains the timing (the round’s end by the feed’s clock, the decision, the send) on every bet, and what happened to each order.

gate

gate(portfolio: PortfolioConfig, state: CardState, s: Settings) -> Reason | None

What stops a new bet (option A: strategy 4 answers to its own money controls only): the shared kill switch, and its stop loss on this card’s settled rounds P&L in its mode. The older strategies’ gates (bankroll cap, card loss, drawdown) never apply.

moment

moment(fight: Fight, round_idx: int, now: dt.datetime, start: dt.datetime | None, feed_age_seconds: float, round_end: dt.datetime | None = None) -> Moment | None

The decision point, or None when this strategy has none here. round_end: when the feed’s clock says the round before ended (for the latency record).

observe

observe(fight_id: str, now: dt.datetime, book_a: BookTop | None, book_b: BookTop | None) -> None

A pre-fight sample of the dataset’s A winner mid (Kalshi’s books A and B).

prepare

prepare(fights: Sequence[Fight], data: RoundsData, winner_books: Mapping[str, tuple[BookTop | None, BookTop | None]]) -> None

Link the card’s fights and build their pre-fight rows (in a worker thread: the fighter histories take a while). winner_books: Kalshi fight id -> its two winner books now (the card-start quotes, before any fight has started).

settings

settings(portfolio: PortfolioConfig) -> Settings | None

The decision’s settings now (None: no sizing bankroll, nothing trades).

warm

warm(fight_id: str, market: RoundsMarketData, now: dt.datetime) -> None

Keep the fight’s trade tape current between decisions (every WARM_EVERY), so a break reads only its last seconds: from the card run’s first sight of the fight, or the start - 61 minutes once the fight has started.

StoreRoundsData

RoundsData on the rounds data layer: the UFC tables from its loader (services.rounds_inputs.ufc_tables) with the three frames it does not land (the bookmakers’ method odds, the sportsbook lines, the Kalshi card-start prices of past fights) given as pd.read_csv read them; the card’s bouts from upcoming (None: no card, the strategy off with a finding). The card run does not use this class: it is wired with services.rounds_upcoming.StoreRoundsSource (trading.wiring), whose upcoming is the upcoming-card loader (load_upcoming) and whose history is every stored input as it stood before the card.

The upcoming card’s own Kalshi quote is not taken from fights_kalshi (no job derives it for new fights): the run takes it from the live winner books (_with_quotes).

history

history(card_date: dt.date) -> UfcTables

Every UFC table (with_upcoming refuses them if any holds a result on or after the card’s date).

upcoming

upcoming(card_date: dt.date) -> UpcomingCard | None

Functions

fighters_order

fighters_order(a: str, b: str, n1: str, n2: str) -> bool | None

On the one bout the loader linked by ticker: True when Kalshi’s fighter A (a) is the bout’s fighter 1, False when fighter 2, None when the names cannot tell. The words first (orientation); when they cannot tell, the spellings (same_fighter): exactly one order must match both fighters. Never used to FIND a bout.

last_completed_card

last_completed_card(history: UfcTables, before: dt.date) -> dt.date | None

The date of the latest card before before with results in the UFCStats tables.

link_fights(fights: Sequence[Fight], card: UpcomingCard) -> tuple[dict[str, FightLink], Counter[str]]

Each Kalshi fight’s bout on the card.

A card the upcoming-card loader assembled (its bouts carry event_ticker, the Kalshi fight the loader linked each to after its own checks): ONLY the bout naming the fight’s ticker. A fight no bout names — the loader excluded it, or never linked it — is not traded; names are never tried (they would skip the loader’s checks). The names then give only the order of the fighters on that one bout (fighters_order).

A card without tickers (a research-built card: replays, tests): the names, strictly — both fighters sharing a name word (over two letters) with the bout’s two names, in either order, exactly one bout, the order by the words (orientation).

Either way the order must be clear, the scheduled rounds must agree and no two Kalshi fights may take one bout; anything else links nothing (never a guess). Names and tickers only, never an outcome: the forward check links the stored card the same way.

market_names

market_names(swap: bool) -> dict[Role, str]

Kalshi role (framed on Kalshi’s fighter A) -> the dataset’s market name (framed on the dataset’s fighter A).

orientation

orientation(ka: frozenset[str], kb: frozenset[str], t1: frozenset[str], t2: frozenset[str]) -> bool | None

True when Kalshi’s fighter A is the bout’s fighter 1, False when fighter 2, None when the names cannot tell. When both orders share a word (Jose Aldo vs Jose Mariscal), only the words the two fighters do NOT share decide: the order matching more of them wins; a tie (identical names) is ambiguous.

resolve_money

resolve_money(portfolio: PortfolioConfig) -> MoneyLimits | None

The ONE place strategy 4’s money controls become dollar limits: the sizing bankroll (None -> None: the strategy does not trade), the card cap and the stop loss, each given in dollars (‘usd’) or as a share of the sizing bankroll (‘pct’); a control left NULL is unlimited. The live cash cap is not a setting (RoundsRunner.finalize).

same_fighter

same_fighter(kalshi: str, ufc: str) -> bool

One fighter’s two spellings: a shared name word (over two letters), or the same letters once the spaces go (‘Alateng Heili’ = ‘Alatengheili’), or a word of four letters or more of one inside the other’s letters (‘Heili’ in ‘Alatengheili’).

settings_of

settings_of(cfg: RoundsConfig, portfolio: PortfolioConfig) -> Settings | None

The decision’s settings: the strategy row’s optimised core and the portfolio’s money controls in dollars (resolve_money); None without a sizing bankroll.