Execution
An Executor accepts a BetIntent and returns a Position. Swap Simulator for a live exchange without changing your agent code.
Overview
| Executor | Description |
|---|---|
Simulator | Paper trading: tracks P&L in memory. No API key, no network. |
ExchangeExecutor | Abstract 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.
| Field | Type | Description |
|---|---|---|
event_id | str | Event ID as returned by a provider. |
market_type | str | Market slug, e.g. "winner", "totals". |
outcome_label | str | Outcome label, e.g. "Home", "Over". |
side | Side | Side.BACK (traditional bet) or Side.LAY (exchange). |
stake | float | Amount to wager in the account's base currency. |
min_odds | float | Refuse execution if offered odds fall below this value. |
max_odds | float? | Optional ceiling (useful for exchange limit orders). |
line | float? | Handicap or total line when relevant. |
notes | str? | 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.
| Field | Type | Description |
|---|---|---|
id | str | Auto-generated 8-character UUID prefix. |
event_id | str | Event the bet was placed on. |
market_type | str | Market slug. |
outcome_label | str | Outcome backed or laid. |
side | Side | BACK or LAY. |
stake | float | Amount wagered. |
odds_taken | float | Actual decimal odds at execution. |
placed_at | datetime | UTC timestamp of placement. |
status | PositionStatus | PENDING, WON, LOST, VOID, CASHOUT. |
settlement_value | float? | Net P&L after settlement (positive = profit, negative = loss). |
settled_at | datetime? | UTC timestamp of settlement. |
notes | str? | Forwarded from BetIntent. |
Computed properties on Position:
| Property | Description |
|---|---|
potential_profit | stake × (odds_taken - 1) |
potential_return | stake × 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)| Parameter | Default | Description |
|---|---|---|
bankroll | 1000.0 | Starting balance. |
commission_rate | 0.0 | Commission on net profit (0.0–1.0). E.g. 0.05 = 5% Betfair-style. |
verbose | True | Log each action at INFO level. |
Methods
| Method | Returns | Description |
|---|---|---|
place(intent) | Position | Fill the intent. Debits stake from balance. Raises InsufficientFundsError or OddsMovedError. |
cancel(position_id) | bool | Cancel an open position. Refunds stake. Returns False if already settled. |
get_position(position_id) | Position | Look up a position by ID. |
get_balance() | float | Current 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) | Position | Settle an individual position. Credits balance if won. |
settle_event(event_id, results) | list[Position] | Bulk-settle all open positions on an event. |
total_exposure() | float | Sum of stakes across all open positions. |
realized_pnl() | float | Net profit/loss across all settled positions. |
summary() | dict | Stats dict: balance, P&L, ROI, win rate, exposure. |
print_summary() | None | Print 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
...| Parameter | Default | Description |
|---|---|---|
api_key | env var | Exchange API key. Falls back to OPENSPORT_EXCHANGE_API_KEY. |
max_single_stake | 100.0 | Hard cap on any single bet stake. |
max_total_exposure | 1000.0 | Abort if aggregate open stakes would exceed this amount. |
dry_run | True | Log 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
| Exception | When raised |
|---|---|
ExecutionError | Base class for all execution failures. |
InsufficientFundsError | Stake exceeds available balance. |
OddsMovedError | Available odds dropped below intent.min_odds. |
PositionNotFoundError | Position 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.