OSopensport.dev
Reference

Execution

An Executor accepts a BetIntent and returns a Position. Swap Simulator for a live exchange without changing your agent code.

Overview

ExecutorDescription
SimulatorPaper trading: tracks P&L in memory. No API key, no network.
ExchangeExecutorAbstract skeleton for live exchange connectors. Subclass and implement three API methods.

Core models

BetIntent

Created by an agent and passed to an executor. Expresses what the agent wants to do but does not commit any funds.

FieldTypeDescription
event_idstrEvent ID as returned by a provider.
market_typestrMarket slug, e.g. "winner", "totals".
outcome_labelstrOutcome label, e.g. "Home", "Over".
sideSideSide.BACK (traditional bet) or Side.LAY (exchange).
stakefloatAmount to wager in the account's base currency.
min_oddsfloatRefuse execution if offered odds fall below this value.
max_oddsfloat?Optional ceiling (useful for exchange limit orders).
linefloat?Handicap or total line when relevant.
notesstr?Agent's reasoning, preserved for logging and explainability.

Position

Created by an executor after accepting a BetIntent. Records the actual odds taken and is updated to WON, LOST, or VOID on settlement.

FieldTypeDescription
idstrAuto-generated 8-character UUID prefix.
event_idstrEvent the bet was placed on.
market_typestrMarket slug.
outcome_labelstrOutcome backed or laid.
sideSideBACK or LAY.
stakefloatAmount wagered.
odds_takenfloatActual decimal odds at execution.
placed_atdatetimeUTC timestamp of placement.
statusPositionStatusPENDING, WON, LOST, VOID, CASHOUT.
settlement_valuefloat?Net P&L after settlement (positive = profit, negative = loss).
settled_atdatetime?UTC timestamp of settlement.
notesstr?Forwarded from BetIntent.

Computed properties on Position:

PropertyDescription
potential_profitstake × (odds_taken - 1)
potential_returnstake × odds_taken
is_open()True when status is PENDING

Simulator

Paper trading engine. Fills every BetIntent at min_odds(conservative fill — no slippage improvement). All state is in memory.

from opensport.execution.simulator import Simulator

sim = Simulator(bankroll=1_000.0, commission_rate=0.05, verbose=True)
ParameterDefaultDescription
bankroll1000.0Starting balance.
commission_rate0.0Commission on net profit (0.0–1.0). E.g. 0.05 = 5% Betfair-style.
verboseTrueLog each action at INFO level.

Methods

MethodReturnsDescription
place(intent)PositionFill the intent. Debits stake from balance. Raises InsufficientFundsError or OddsMovedError.
cancel(position_id)boolCancel an open position. Refunds stake. Returns False if already settled.
get_position(position_id)PositionLook up a position by ID.
get_balance()floatCurrent balance.
get_all_positions()list[Position]All positions (open and settled).
get_open_positions()list[Position]Only positions with PENDING status.
settle_position(id, won)PositionSettle an individual position. Credits balance if won.
settle_event(event_id, results)list[Position]Bulk-settle all open positions on an event.
total_exposure()floatSum of stakes across all open positions.
realized_pnl()floatNet profit/loss across all settled positions.
summary()dictStats dict: balance, P&L, ROI, win rate, exposure.
print_summary()NonePrint a formatted summary to stdout.

Settlement examples

# Settle one position
sim.settle_position(pos.id, won=True)

# Bulk-settle an entire event
sim.settle_event("mock_soccer_000", {
    "Home": True,
    "Draw": False,
    "Away": False,
})

# Cancel an open position (refunds stake)
sim.cancel(pos.id)

ExchangeExecutor

Abstract skeleton for live exchange connectors. Provides logging, risk guards, and position tracking. Subclasses only need to implement three API methods.

from opensport.execution.exchange import ExchangeExecutor
from opensport.core.position import BetIntent, Position

class MyExchangeExecutor(ExchangeExecutor):
    name = "my_exchange"

    def _api_place_bet(self, intent: BetIntent) -> Position:
        # call your exchange's order placement API
        # map the response to a Position and return it
        ...

    def _api_cancel_order(self, position_id: str) -> bool:
        # call your exchange's cancellation API
        # return True if cancelled, False if already matched
        ...

    def _api_get_balance(self) -> float:
        # call your exchange's balance endpoint
        ...
ParameterDefaultDescription
api_keyenv varExchange API key. Falls back to OPENSPORT_EXCHANGE_API_KEY.
max_single_stake100.0Hard cap on any single bet stake.
max_total_exposure1000.0Abort if aggregate open stakes would exceed this amount.
dry_runTrueLog intended actions without calling the exchange API. Set to False only when ready for live execution.

The dry_run default is True intentionally. You must explicitly set dry_run=False to place real bets.

BaseExecutor interface

Implement BaseExecutor directly if you need full control without theExchangeExecutor plumbing.

from opensport.execution.base import BaseExecutor
from opensport.core.position import BetIntent, Position

class BaseExecutor(ABC):
    name: str = "base"

    def place(intent: BetIntent) -> Position: ...
    def cancel(position_id: str) -> bool: ...
    def get_position(position_id: str) -> Position: ...
    def get_balance() -> float: ...

    # Optional helpers with default implementations:
    def get_all_positions() -> list[Position]: ...
    def get_open_positions() -> list[Position]: ...
    def total_exposure() -> float: ...
    def realized_pnl() -> float: ...

Exceptions

ExceptionWhen raised
ExecutionErrorBase class for all execution failures.
InsufficientFundsErrorStake exceeds available balance.
OddsMovedErrorAvailable odds dropped below intent.min_odds.
PositionNotFoundErrorPosition ID does not exist in the executor.
from opensport.execution.base import InsufficientFundsError, OddsMovedError

try:
    pos = sim.place(intent)
except InsufficientFundsError:
    print("Not enough balance")
except OddsMovedError:
    print("Odds dropped — intent rejected")

See the Agents reference for how agents wire together providers and executors, and the Architecture guide for the full system overview.