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
FightLink
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 | Noneprints
prints(market_id: str, since: dt.datetime, until: dt.datetime) -> list[Print] | NoneMoment
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) -> UfcTablesEvery 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 | NoneThe 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 | Noneprints
prints(market_id: str, since: dt.datetime, until: dt.datetime) -> list[Print] | NoneRoundsRunner
Strategy 4 for one card run.
card_state
card_state(orders: Mapping[str, OrderRecord], settlements: Mapping[str, SettlementRecord]) -> CardStateThis 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 | NoneWhat 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 | NoneThe 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) -> NoneA 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]]) -> NoneLink 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 | NoneThe decision’s settings now (None: no sizing bankroll, nothing trades).
warm
warm(fight_id: str, market: RoundsMarketData, now: dt.datetime) -> NoneKeep 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) -> UfcTablesEvery 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 | NoneFunctions
fighters_order
fighters_order(a: str, b: str, n1: str, n2: str) -> bool | NoneOn 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 | NoneThe date of the latest card before before with results in the UFCStats tables.
link_fights
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 | NoneTrue 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 | NoneThe 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) -> boolOne 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 | NoneThe decision’s settings: the strategy row’s optimised core and the portfolio’s money
controls in dollars (resolve_money); None without a sizing bankroll.