docs/integrations/betfair.md
Founded in 2000, Betfair operates the world’s largest online betting exchange, with its headquarters in London and satellite offices across the globe.
NautilusTrader provides an adapter for integrating with the Betfair REST API and Exchange Streaming API.
Install NautilusTrader with Betfair support:
uv pip install "nautilus_trader[betfair]"
To build from source with Betfair extras:
uv sync --all-extras
You can find live example scripts here.
Betfair provides documentation for developers:
Betfair requires an Application Key to authenticate API requests. After registering and funding your account, obtain your key using the API-NG Developer AppKeys Tool.
Two App Keys are assigned per account: a Live key (requires a one-time activation fee) and a Delayed key for development and testing.
:::info See the Application Keys documentation for detailed setup instructions. :::
Supply your Betfair credentials via environment variables or client configuration:
export BETFAIR_USERNAME=<your_username>
export BETFAIR_PASSWORD=<your_password>
export BETFAIR_APP_KEY=<your_app_key>
export BETFAIR_CERTS_DIR=<path_to_certificate_dir>
:::tip We recommend using environment variables to manage your credentials. :::
:::note
Current Rust note: Rust currently reads BETFAIR_USERNAME, BETFAIR_PASSWORD, and
BETFAIR_APP_KEY. It does not yet read BETFAIR_CERTS_DIR.
:::
Betfair recommends non-interactive (bot) login
with SSL certificates for automated trading systems. The certs_dir configuration is optional,
but certificates are recommended for production deployments.
Create a 2048-bit RSA certificate using OpenSSL:
# Generate private key and certificate signing request
openssl genrsa -out client-2048.key 2048
openssl req -new -key client-2048.key -out client-2048.csr
# Self-sign the certificate (valid for 365 days)
openssl x509 -req -days 365 -in client-2048.csr -signkey client-2048.key -out client-2048.crt
Before using the certificate, attach it to your Betfair account:
client-2048.crt file.Place your certificate files in a directory and set BETFAIR_CERTS_DIR to that path:
/path/to/certs/
├── client-2048.crt
└── client-2048.key
:::info SSL certificates are used for the Exchange Streaming API connection. The REST API uses username/password authentication with your Application Key. :::
:::warning Enabling 2-Step Authentication on the Betfair website does not affect API access. Certificate-based login remains functional regardless of 2FA settings. :::
The Betfair adapter provides three primary components:
BetfairInstrumentProvider: loads Betfair markets and converts them into Nautilus instruments.BetfairDataClient: streams real-time market data from the Exchange Streaming API.BetfairExecutionClient: submits orders (bets) and tracks execution status via the REST API.NautilusTrader currently ships a stable Python Betfair adapter and an in-progress Rust parity path.
This page remains the stable guide. It now calls out the main Rust differences inline. Use the
Betfair v2 transition guide for the current Rust-first behavior in
crates/adapters/betfair and for the planned cutover path.
Betfair operates as a betting exchange with unique characteristics compared to traditional financial exchanges:
| Order Type | Supported | Notes |
|---|---|---|
MARKET | ✓* | Python maps regular market orders to aggressive LIMIT; Rust supports BSP AT_THE_CLOSE only. |
LIMIT | ✓ | Orders placed at specific odds. |
STOP_MARKET | - | Not supported. |
STOP_LIMIT | - | Not supported. |
MARKET_IF_TOUCHED | - | Not supported. |
LIMIT_IF_TOUCHED | - | Not supported. |
TRAILING_STOP_MARKET | - | Not supported. |
| Instruction | Supported | Notes |
|---|---|---|
post_only | - | Not applicable to betting exchange. |
reduce_only | - | Not applicable to betting exchange. |
| Time in force | Supported | Notes |
|---|---|---|
GTC | ✓ | Maps to Betfair PERSIST persistence. |
GTD | - | Not supported. |
DAY | ✓ | Maps to Betfair LAPSE persistence. |
FOK | ✓ | Maps to Betfair FILL_OR_KILL. |
IOC | ✓ | Maps to FILL_OR_KILL with partial fills. |
:::note
Betfair uses a persistence model rather than traditional time-in-force. The adapter maps FOK to
Betfair's FILL_OR_KILL, while IOC uses FILL_OR_KILL with min_fill_size=0 to allow partial fills.
The current adapters also support BSP on-close order flows. Rust only accepts MARKET orders in
AT_THE_CLOSE mode. Rust also maps LIMIT orders in AT_THE_CLOSE or AT_THE_OPEN mode to
Betfair LIMIT_ON_CLOSE instructions.
:::
| Feature | Supported | Notes |
|---|---|---|
| Order Modification | ✓ | Limited to non‑exposure changing fields. |
| Bracket/OCO Orders | - | Not supported. |
| Iceberg Orders | - | Not supported. |
| Operation | Supported | Notes |
|---|---|---|
| Batch Submit | ✓ | Supports SubmitOrderList in Python and Rust. |
| Batch Modify | - | Not supported. |
| Batch Cancel | ✓ | Supports batched cancel requests in Python and Rust. |
| Feature | Supported | Notes |
|---|---|---|
| Query positions | - | Betting exchange model differs. |
| Position mode | - | Not applicable to betting exchange. |
| Leverage control | - | No leverage in betting exchange. |
| Margin mode | - | No margin in betting exchange. |
| Feature | Supported | Notes |
|---|---|---|
| Query open orders | ✓ | List all active bets. |
| Query order history | ✓ | Historical betting data. |
| Order status updates | ✓ | Real‑time bet state changes. |
| Trade history | ✓ | Bet matching and settlement reports. |
| Feature | Supported | Notes |
|---|---|---|
| Order lists | - | Not supported. |
| OCO orders | - | Not supported. |
| Bracket orders | - | Not supported. |
| Conditional orders | - | Basic bet conditions only. |
Betfair uses a tiered tick scheme with varying increments across price ranges:
| Price Range | Tick Size |
|---|---|
| 1.01 - 2.00 | 0.01 |
| 2.00 - 3.00 | 0.02 |
| 3.00 - 4.00 | 0.05 |
| 4.00 - 6.00 | 0.10 |
| 6.00 - 10.00 | 0.20 |
| 10.00 - 20.00 | 0.50 |
| 20.00 - 30.00 | 1.00 |
| 30.00 - 50.00 | 2.00 |
| 50.00 - 100.00 | 5.00 |
| 100.00 - 1000.00 | 10.00 |
The minimum price is 1.01 and the maximum is 1000.00.
Order modification on Betfair has specific constraints:
ReplaceOrders (cancel + new order at new price).CancelOrders with a size_reduction parameter.:::warning A replace operation generates both a cancel event for the original order and an accepted event for the replacement order. The adapter tracks pending replacements to suppress synthetic cancel events. :::
The execution client processes order updates from the Betfair Exchange Streaming API. Two configuration options control how updates are filtered:
stream_market_ids_filter: Filters at the market level (early exit, silent skip).ignore_external_orders: Filters at the order level. Python also uses it to control the
log level for full-image cache checks. Rust currently only skips OCM updates with no rfo.The flowchart below matches the stable Python execution path.
Python keeps stream_market_ids_filter separate from reconciliation scope (reconcile_market_ids_only).
Rust currently falls back to stream_market_ids_filter during reconciliation when
reconcile_market_ids_only=False and no explicit reconcile_market_ids are configured.
flowchart TD
A[Stream update arrives] --> B{Market in
stream_market_ids_filter?}
B -->|No filter set| C{Instrument loaded?}
B -->|Yes| C
B -->|No| D[Skip silently]
C -->|No| E[Warning: Instrument not loaded]
C -->|Yes| F{Known order?
rfo or cache}
F -->|Yes| G[Process order update]
F -->|No| H{ignore_external_orders?}
H -->|True| I[Debug log, skip]
H -->|False| J[Warning log, skip]
Python also applies stream_market_ids_filter during full-image reconciliation in
check_cache_against_order_image. Rust currently reconciles through generate_mass_status() and
does not yet perform the same full-image cache check.
When ignore_external_orders=True, the Python adapter skips orders and fills not found in cache:
| Scenario | Description |
|---|---|
| Unknown order in stream update | No venue‑to‑client order ID mapping exists. |
| Unknown order in full image | Order not found in cache during image sync. |
| Unknown fill in full image | Fill does not match any known order during sync. |
:::info
For multi-node setups sharing a Betfair account, set both stream_market_ids_filter (your markets only)
and ignore_external_orders=True to avoid warnings about orders managed by other nodes.
:::
The adapter handles several edge cases when processing fills from the stream:
The adapter uses separate rate limit buckets so that account state polling and reconciliation do not throttle order placement:
| Bucket | Default | Endpoints | Configurable |
|---|---|---|---|
| General | 5/s | Account state, reconciliation, keep‑alive. | |
| Orders | 20/s | placeOrders, replaceOrders, cancelOrders. | order_request_rate_per_second. |
Order status and fill report queries retry once on TOO_MANY_REQUESTS errors
after a 1-second delay; order operations reject with the error message.
Betfair's actual API limits are more nuanced:
| Category | Limit | Notes |
|---|---|---|
| Order operations | 1,000 transactions/s | Total instructions across placeOrders, cancelOrders, replaceOrders. |
| Order projection queries | 3 concurrent | listMarketBook (with OrderProjection), listCurrentOrders, listMarketProfitAndLoss. |
| Best practice | 5 requests/s | Recommended for listMarketBook per market. |
:::info For details on rate limits, see Why am I receiving the TOO_MANY_REQUESTS error? and Market Data Request Limits. :::
The Betfair adapter provides several custom data types that flow through the market stream. All custom data is delivered automatically when subscribed to markets - no explicit subscription is required, though strategies can register handlers for specific data types.
Real-time ticker data for a betting selection.
| Field | Type | Description |
|---|---|---|
instrument_id | str | Nautilus instrument identifier. |
last_traded_price | float | Last matched price (odds). |
traded_volume | float | Total matched volume. |
starting_price_near | float | Near‑side BSP indicator. |
starting_price_far | float | Far‑side BSP indicator. |
The realized Betfair Starting Price (BSP) after market close.
| Field | Type | Description |
|---|---|---|
instrument_id | str | Nautilus instrument identifier. |
bsp | float | Final starting price (odds). |
Live GPS tracking data for individual horses (Total Performance Data). Available for supported UK and Irish races.
| Field | Type | Description |
|---|---|---|
race_id | str | Betfair race identifier. |
market_id | str | Betfair market identifier. |
selection_id | int | Betfair selection (runner) identifier. |
latitude | float | GPS latitude. |
longitude | float | GPS longitude. |
speed | float | Current speed in m/s (Doppler‑derived). |
progress | float | Distance to finish line in meters. |
stride_frequency | float | Stride frequency in Hz. |
Race summary data with sectional times and running order.
| Field | Type | Description |
|---|---|---|
race_id | str | Betfair race identifier. |
market_id | str | Betfair market identifier. |
gate_name | str | Timing gate (e.g., "1f", "2f", "Finish"). |
sectional_time | float | Time for this section in seconds. |
running_time | float | Total time since race start in seconds. |
speed | float | Lead horse speed in m/s. |
progress | float | Lead horse distance to finish in meters. |
order | list[int] | Selection IDs in current race position order. |
jumps | list[dict] | Jump obstacle data for National Hunt races. |
Custom data flows automatically through the Betfair market stream when you subscribe to markets. To receive custom data in your strategy or actor, register a handler with the Betfair client ID:
from nautilus_trader.adapters.betfair.constants import BETFAIR_CLIENT_ID
from nautilus_trader.adapters.betfair.data_types import BetfairRaceRunnerData
from nautilus_trader.adapters.betfair.data_types import BetfairRaceProgress
from nautilus_trader.adapters.betfair.data_types import BetfairTicker
from nautilus_trader.model.data import DataType
class MyStrategy(Strategy):
def on_start(self):
# Subscribe to ticker data
self.subscribe_data(DataType(BetfairTicker), client_id=BETFAIR_CLIENT_ID)
# Subscribe to ALL race runner data (wildcard)
self.subscribe_data(DataType(BetfairRaceRunnerData), client_id=BETFAIR_CLIENT_ID)
# Or subscribe to a specific runner by selection_id
self.subscribe_data(
DataType(BetfairRaceRunnerData, metadata={"selection_id": 49411491}),
client_id=BETFAIR_CLIENT_ID,
)
# Subscribe to ALL race progress updates (wildcard)
self.subscribe_data(DataType(BetfairRaceProgress), client_id=BETFAIR_CLIENT_ID)
# Or subscribe to a specific race by race_id
self.subscribe_data(
DataType(BetfairRaceProgress, metadata={"race_id": "35278018.1617"}),
client_id=BETFAIR_CLIENT_ID,
)
def on_data(self, data):
if isinstance(data, BetfairRaceRunnerData):
self.log.info(
f"Runner {data.selection_id}: speed={data.speed} m/s, "
f"progress={data.progress}m to finish"
)
elif isinstance(data, BetfairRaceProgress):
self.log.info(f"Race order: {data.order}")
elif isinstance(data, BetfairTicker):
self.log.info(f"LTP: {data.last_traded_price}")
:::info
Subscribing with DataType(BetfairRaceRunnerData) (no metadata) receives data for
all runners. Adding metadata={"selection_id": <id>} filters to a specific runner.
Similarly, DataType(BetfairRaceProgress) receives progress for all races, while
metadata={"race_id": <id>} filters to a specific race.
Race data (RCM messages) requires Total Performance Data (TPD) coverage and a Betfair API key with TPD access. Not all races have GPS tracking enabled. :::
For backtesting with recorded race data, use the file parser:
from nautilus_trader.adapters.betfair.parsing.core import parse_betfair_rcm_file
for data in parse_betfair_rcm_file("path/to/rcm_data.json"):
if isinstance(data, BetfairRaceRunnerData):
print(f"Runner {data.selection_id} at {data.latitude}, {data.longitude}")
| Option | Default | Description |
|---|---|---|
account_currency | Required | Betfair account currency for data and price feeds. |
username | None | Betfair account username; taken from environment when omitted. |
password | None | Betfair account password; taken from environment when omitted. |
app_key | None | Betfair application key used for API authentication. |
certs_dir | None | Directory containing Betfair SSL certificates for login. |
instrument_config | None | Optional BetfairInstrumentProviderConfig to scope available markets. |
subscription_delay_secs | 3 | Delay (seconds) before initial market subscription request is sent. |
keep_alive_secs | 36,000 | Keep‑alive interval (seconds) for the Betfair session. |
subscribe_race_data | False | When True, subscribe to Race Change Messages (RCM) for live GPS tracking data. |
stream_conflate_ms | None | Explicit stream conflation interval in milliseconds (0 disables conflation). |
stream_heartbeat_ms | 5,000 | Stream heartbeat interval in milliseconds (500-5000). None to omit. |
proxy_url | None | Optional proxy URL for HTTP requests. |
:::warning
When stream_conflate_ms is None, Betfair applies its default conflation behavior (typically enabled).
Set stream_conflate_ms=0 explicitly to guarantee no conflation and receive every price update.
:::
:::note Current Rust differences:
certs_dir.instrument_config; it scopes instruments with direct filter fields on BetfairDataConfig.stream_heartbeat_ms; it does not accept None to omit the heartbeat.:::
| Option | Default | Description |
|---|---|---|
account_currency | Required | Betfair account currency for order placement and balances. |
username | None | Betfair account username; taken from environment when omitted. |
password | None | Betfair account password; taken from environment when omitted. |
app_key | None | Betfair application key used for API authentication. |
certs_dir | None | Directory containing Betfair SSL certificates for login. |
instrument_config | None | Optional BetfairInstrumentProviderConfig to scope reconciliation. |
calculate_account_state | True | Calculate account state locally from events when True. |
request_account_state_secs | 300 | Interval (seconds) to poll Betfair for account state (0 disables). |
reconcile_market_ids_only | False | When True, reconciliation only covers instrument_config.market_ids (no effect if unset). |
reconcile_market_ids | None | Rust only. Explicit market IDs to use for reconciliation when reconcile_market_ids_only=True. |
stream_market_ids_filter | None | List of market IDs to process from stream; others are silently skipped. |
ignore_external_orders | False | When True, ignore stream orders missing from the local cache. |
use_market_version | False | When True, attach the latest market version to order requests for price protection. |
order_request_rate_per_second | 20 | Rate limit (requests/second) for order endpoints, separate from general API endpoints. |
stream_heartbeat_ms | 5,000 | Order stream heartbeat interval in milliseconds (500-5000). None to omit. |
proxy_url | None | Optional proxy URL for HTTP requests. |
:::warning
If you set stream_market_ids_filter, ensure it includes all markets you trade. Orders placed on
markets excluded from this filter will miss live fill and cancel updates from the stream.
:::
:::note Current Rust differences:
certs_dir or instrument_config.calculate_account_state as the gate for periodic account-state polling.reconcile_market_ids when reconcile_market_ids_only=True.reconcile_market_ids_only=False, Rust currently falls back to stream_market_ids_filter
for startup reconciliation when reconcile_market_ids is unset.ignore_external_orders only to OCM updates with no rfo.stream_heartbeat_ms; it does not accept None to omit the heartbeat.:::
Betfair sessions typically expire every 12-24 hours. The adapter automatically handles session
reconnection when NO_SESSION or INVALID_SESSION_INFORMATION errors occur:
keep_alive_secs. Rust
currently uses a fixed 10-hour interval.:::info Session errors during account state polling or keep-alive trigger automatic reconnection. No manual intervention is required for normal session expiry. :::
Betfair markets have a version number that increments whenever the market book changes
(e.g., a new price level appears, a bet is matched). The adapter can attach this version
to placeOrders and replaceOrders requests, providing price protection against stale orders.
When use_market_version=True, each order request includes the market version last seen
by the adapter. If the market has advanced beyond that version by the time Betfair processes
the order, Betfair lapses the bet rather than matching it against a changed book.
from nautilus_trader.adapters.betfair.config import BetfairExecClientConfig
exec_config = BetfairExecClientConfig(
account_currency="GBP",
use_market_version=True,
)
The adapter reads the market version from the instrument's info dictionary, which
the Exchange Streaming API's MarketDefinition updates populate. This means:
MarketDefinition is received will not include a version.:::warning Market version protection is conservative. In fast-moving markets, the version may advance between your order signal and submission, causing the bet to lapse even though the price is still acceptable. Consider this trade-off between protection and fill rate. :::
When multiple trading nodes share a single Betfair account across different markets, configure each node to avoid interference:
stream_market_ids_filter to include only that node's markets.ignore_external_orders=True to suppress warnings about orders from other nodes.reconcile_market_ids_only=True to limit reconciliation scope.This prevents warning spam and ensures each node processes only its own orders and fills.
Here is a minimal example showing how to configure a live TradingNode with Betfair clients:
from nautilus_trader.adapters.betfair import BETFAIR
from nautilus_trader.adapters.betfair import BetfairLiveDataClientFactory
from nautilus_trader.adapters.betfair import BetfairLiveExecClientFactory
from nautilus_trader.config import TradingNodeConfig
from nautilus_trader.live.node import TradingNode
# Configure Betfair data and execution clients (using AUD account currency)
config = TradingNodeConfig(
data_clients={BETFAIR: {"account_currency": "AUD"}},
exec_clients={BETFAIR: {"account_currency": "AUD"}},
)
# Build the TradingNode with Betfair adapter factories
node = TradingNode(config)
node.add_data_client_factory(BETFAIR, BetfairLiveDataClientFactory)
node.add_exec_client_factory(BETFAIR, BetfairLiveExecClientFactory)
node.build()
:::info For additional features or to contribute to the Betfair adapter, please see our contributing guide. :::