Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Release History
---------------

3.2.4 (2026-09-28)
+++++++++++++++++++

**Improvements**

- TOTE client/market logic added

3.2.3 (2026-09-25)
+++++++++++++++++++

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ flumine is an open-source, event-based trading framework for sports betting, des
- Betfair
- Betdaq
- Betconnect (account/execution functionality)
- Tote (in development)
- Smarkets (roadmap)
- Matchbook (roadmap)
- Polymarket (roadmap)
- Kalshi (roadmap)
- BetDEX (roadmap)
- Tote (roadmap)


![Backtesting Analysis](docs/images/jupyterloggingcontrol-screenshot.png?raw=true "Jupyter Logging Control Screenshot")
Expand Down
1 change: 1 addition & 0 deletions docs/version_migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This means an instance can be setup to execute on either individual venues or mu
- Betfair: Completed
- BETDAQ: Completed
- BetConnect: Development
- Tote: Development
- Polymarket: Roadmap
- Kalshi: Roadmap
- BetDEX: Roadmap
Expand Down
2 changes: 1 addition & 1 deletion flumine/__version__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__title__ = "flumine"
__description__ = "Betting trading framework"
__url__ = "https://github.com/betcode-org/flumine"
__version__ = "3.2.3"
__version__ = "3.2.4"
__author__ = "Liam Pauling"
__license__ = "MIT"
11 changes: 9 additions & 2 deletions flumine/baseflumine.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .worker import BackgroundWorker
from .clients import Clients, BaseClient, VenueType
from .markets.markets import Markets
from .markets.market import Market, BetdaqMarket
from .markets.market import Market, BetdaqMarket, ToteMarket
from .markets.middleware import Middleware, SimulatedMiddleware
from .execution.betfairexecution import BetfairExecution
from .execution.simulatedexecution import SimulatedExecution
Expand Down Expand Up @@ -166,6 +166,9 @@ def _process_market_books(self, event: events.MarketBookEvent) -> None:
elif event.venue == VenueType.BETDAQ:
market_id = market_book["market_id"]
streaming_unique_id = market_book["streaming_unique_id"]
elif event.venue == VenueType.TOTE:
market_id = market_book["id"]
streaming_unique_id = market_book["streaming_unique_id"]
else:
raise NotImplementedError()

Expand All @@ -186,8 +189,10 @@ def _process_market_books(self, event: events.MarketBookEvent) -> None:
market_is_new = market is None
if market_is_new:
market = self._add_market(market_id, market_book, event.venue)
if market.VENUE == VenueType.TOTE:
self.log_control(events.MarketEvent(market))
elif market.closed:
if event.venue == VenueType.BETDAQ:
if event.venue != VenueType.BETFAIR:
continue
self.markets.add_market(market_id, market)

Expand Down Expand Up @@ -265,6 +270,8 @@ def _add_market(
market = Market(self, market_id, market_book)
elif venue_type == VenueType.BETDAQ:
market = BetdaqMarket(self, market_id, market_book)
elif venue_type == VenueType.TOTE:
market = ToteMarket(self, market_id, market_book)
else:
raise NotImplementedError()
self.markets.add_market(market_id, market)
Expand Down
1 change: 1 addition & 0 deletions flumine/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .simulatedclient import SimulatedClient
from .betconnectclient import BetConnectClient
from .betdaqclient import BetdaqClient
from .toteclient import ToteClient
35 changes: 35 additions & 0 deletions flumine/clients/toteclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging
from typing import Optional
from flumine.clients.baseclient import BaseClient
from flumine.clients import VenueType

logger = logging.getLogger(__name__)


class ToteClient(BaseClient):
"""
Tote betting client.
"""

VENUE = VenueType.TOTE

def login(self) -> None:
pass

def keep_alive(self) -> None:
pass

def logout(self) -> None:
pass

def update_account_details(self) -> None:
self.account_funds = self.betting_client.customer()

def min_bet_size(self) -> Optional[float]:
return 0.10

def min_bet_payout(self) -> Optional[float]:
return None

def min_bsp_liability(self) -> Optional[float]:
return None
72 changes: 68 additions & 4 deletions flumine/markets/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,6 @@ def event_name(self) -> Optional[str]:
def country_code(self) -> Optional[str]:
return None # todo betdaq catalogue?

@property
def event_venue(self) -> Optional[str]:
return None # todo betdaq catalogue?

@property
def venue(self) -> Optional[str]:
return None # todo betdaq catalogue?
Expand All @@ -373,3 +369,71 @@ def race_type(self) -> Optional[str]:
def status(self) -> Optional[str]:
if self.market_book:
return self.market_book["status"] or "SETTLED"


class ToteMarket(Market):
VENUE = VenueType.TOTE

def __call__(self, market_book: dict):
self.market_book = market_book

@property
def publish_time(self) -> datetime.datetime:
return self.market_book["publish_time"]

@property
def bet_delay(self) -> float:
return 0.0

@property
def market_name(self):
return self.market_book["name"]

@property
def event_type_id(self) -> str:
return None

@property
def event_id(self) -> str:
return 0

@property
def competition_id(self):
return None

@property
def market_type(self) -> str:
return self.market_book["type"]["betType"]["code"].upper()

@property
def market_start_datetime(self):
if self.market_book:
_event = self.market_book["type"]["legs"]["nodes"][0]["event"]
_scheduled_start_date_time = _event["scheduledStartDateTime"]["iso8601"]
return datetime.datetime.fromisoformat(
_scheduled_start_date_time.replace("Z", "+00:00")
)
else:
return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)

@property
def event_name(self) -> Optional[str]:
_event = self.market_book["type"]["legs"]["nodes"][0]["event"]
return _event["id"]

@property
def country_code(self) -> Optional[str]:
return None

@property
def venue(self) -> Optional[str]:
_event = self.market_book["type"]["legs"]["nodes"][0]["event"]
return _event["venue"]["name"].replace("PARK", "").strip().title()

@property
def race_type(self) -> Optional[str]:
return None

@property
def status(self) -> Optional[str]:
return self.market_book["type"]["selling"]["status"]
1 change: 1 addition & 0 deletions tests/resources/bbc663ed-064b-4237-a0ed-0c6e49fdef46.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "bbc663ed-064b-4237-a0ed-0c6e49fdef46", "name": "BRIGHTON RACE 1 - WIN", "type": {"pool": {"total": {"grossAmount": {"decimalAmount": 364.54}, "netAmount": {"decimalAmount": 294.37}}, "takeout": {"percentage": 19.25, "amount": {"decimalAmount": 70.17}}, "carryIn": {"grossAmount": {"decimalAmount": 0.0}, "netAmount": {"decimalAmount": 0.0}}}, "betType": {"id": "0", "code": "WIN", "rules": {"bet": {"min": {"decimalAmount": 0.01, "currency": {"code": "GBP", "name": "Great British Pounds", "symbol": "\\u00a3"}}, "max": {"decimalAmount": 20000, "currency": {"code": "GBP", "name": "Great British Pounds", "symbol": "\\u00a3"}}}, "line": {"min": {"decimalAmount": 0.01, "currency": {"code": "GBP", "name": "Great British Pounds", "symbol": "\\u00a3"}}, "max": {"decimalAmount": 20000, "currency": {"code": "GBP", "name": "Great British Pounds", "symbol": "\\u00a3"}}, "increment": {"decimalAmount": 0.01, "currency": {"code": "GBP", "name": "Great British Pounds", "symbol": "\\u00a3"}}}}}, "selling": {"status": "OPEN"}, "lineHistory": "https://api.tote.rocks/partner/product/line-history/bbc663ed-064b-4237-a0ed-0c6e49fdef46", "lines": {"nodes": [{"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "f085dc2a-6324-43bd-a782-3612fcd56ded"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "b1da35ac-e37d-46d9-960e-931383ea0d4f"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "5ab674a4-461b-4598-9ae8-ed6862659d3a"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "b2e6fbfd-5f14-424c-afd9-14d11acaf777"}]}], "odds": [{"name": "Base", "decimal": 2.6, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 2.964, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "0ebd75ac-c024-4dfe-bf52-af210f41ad63"}]}], "odds": [{"name": "Base", "decimal": 6.3, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 7.182, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "f5699535-2616-48fd-80b0-099de217f6b5"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "eee48647-7415-42f9-8374-fae76ac97a4c"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "ff3aa416-ff32-4199-b47d-918c5ce49439"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "b42ada4d-5643-4888-bc8e-048565f1cf09"}]}], "odds": [{"name": "Base", "decimal": 1.4, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 1.596, "status": "FINAL", "type": "POOL"}]}, {"legs": [{"legId": "6c56b960-2464-4670-9625-171f36d45bd4", "lineSelections": [{"selectionId": "41e28022-59cb-4672-a4d1-5710ee3a1a03"}]}], "odds": [{"name": "Base", "decimal": 0.0, "status": "FINAL", "type": "POOL"}, {"name": "Enhanced", "decimal": 0.0, "status": "FINAL", "type": "POOL"}]}]}, "legs": {"nodes": [{"id": "6c56b960-2464-4670-9625-171f36d45bd4", "event": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230", "name": "BRIGHTON RACE 1", "scheduledStartDateTime": {"iso8601": "2026-09-28T12:30:00Z"}, "venue": {"id": null, "name": "BRIGHTON", "country": {"name": "United Kingdom of Great Britain and Northern Ireland"}}}, "selections": {"nodes": [{"id": "f085dc2a-6324-43bd-a782-3612fcd56ded", "name": "DANGER ALERT", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-1", "name": "DANGER ALERT", "entryStatus": "CONFIRMED", "details": {"clothNumber": 1}}}, {"id": "b1da35ac-e37d-46d9-960e-931383ea0d4f", "name": "DIAMOND DREAMER", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-2", "name": "DIAMOND DREAMER", "entryStatus": "CONFIRMED", "details": {"clothNumber": 2}}}, {"id": "5ab674a4-461b-4598-9ae8-ed6862659d3a", "name": "DESDEMONA", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-3", "name": "DESDEMONA", "entryStatus": "WITHDRAWN", "details": {"clothNumber": 3}}}, {"id": "b2e6fbfd-5f14-424c-afd9-14d11acaf777", "name": "DREAMBIRD DOLLY", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-4", "name": "DREAMBIRD DOLLY", "entryStatus": "CONFIRMED", "details": {"clothNumber": 4}}}, {"id": "0ebd75ac-c024-4dfe-bf52-af210f41ad63", "name": "HARRY BROWN", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-5", "name": "HARRY BROWN", "entryStatus": "CONFIRMED", "details": {"clothNumber": 5}}}, {"id": "f5699535-2616-48fd-80b0-099de217f6b5", "name": "BEACH PARTEE", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-6", "name": "BEACH PARTEE", "entryStatus": "CONFIRMED", "details": {"clothNumber": 6}}}, {"id": "eee48647-7415-42f9-8374-fae76ac97a4c", "name": "ROMAN SPRING", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-7", "name": "ROMAN SPRING", "entryStatus": "CONFIRMED", "details": {"clothNumber": 7}}}, {"id": "ff3aa416-ff32-4199-b47d-918c5ce49439", "name": "STAPLEFORD PARK", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-8", "name": "STAPLEFORD PARK", "entryStatus": "CONFIRMED", "details": {"clothNumber": 8}}}, {"id": "b42ada4d-5643-4888-bc8e-048565f1cf09", "name": "BATED BREEZE", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-9", "name": "BATED BREEZE", "entryStatus": "CONFIRMED", "details": {"clothNumber": 9}}}, {"id": "41e28022-59cb-4672-a4d1-5710ee3a1a03", "name": "ANGLESEY LAD", "competitor": {"id": "HORSERACING-BRIGHTON-GB-2026-09-28-1230-COMPETITOR-10", "name": "ANGLESEY LAD", "entryStatus": "CONFIRMED", "details": {"clothNumber": 10}}}]}}]}}, "publish_time": 123, "streaming_unique_id": 10000}
33 changes: 33 additions & 0 deletions tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SimulatedClient,
BetConnectClient,
BetdaqClient,
ToteClient,
)
from flumine.clients import betfairclient
from flumine import exceptions
Expand Down Expand Up @@ -530,3 +531,35 @@ def test_min_bet_payout(self):

def test_min_bsp_liability(self):
self.assertIsNone(self.betdaq_client.min_bsp_liability())


class ToteClientTest(unittest.TestCase):
def setUp(self):
self.mock_betting_client = mock.Mock()
del self.mock_betting_client.lightweight
self.tote_client = ToteClient(self.mock_betting_client)

def test_login(self):
self.tote_client.login()

def test_keep_alive(self):
self.tote_client.keep_alive()

def test_logout(self):
self.tote_client.logout()

def test_update_account_details(self):
self.tote_client.update_account_details()
self.assertEqual(
self.tote_client.account_funds,
self.mock_betting_client.customer.return_value,
)

def test_min_bet_size(self):
self.assertEqual(self.tote_client.min_bet_size(), 0.10)

def test_min_bet_payout(self):
self.assertIsNone(self.tote_client.min_bet_payout())

def test_min_bsp_liability(self):
self.assertIsNone(self.tote_client.min_bsp_liability())
70 changes: 67 additions & 3 deletions tests/test_markets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import unittest
import datetime
import threading
Expand All @@ -6,7 +7,7 @@

from flumine.clients import VenueType
from flumine.markets.markets import Markets
from flumine.markets.market import Market, BetdaqMarket
from flumine.markets.market import Market, BetdaqMarket, ToteMarket
from flumine import config


Expand Down Expand Up @@ -633,11 +634,74 @@ def test_event_name(self):
def test_country_code(self):
self.assertIsNone(self.market.country_code)

def test_event_venue(self):
self.assertIsNone(self.market.event_venue)
def test_venue(self):
self.assertIsNone(self.market.venue)

def test_race_type(self):
self.assertIsNone(self.market.race_type)

def test_status(self):
self.assertEqual(self.market.status, "ACTIVE")


class ToteMarketTest(unittest.TestCase):
def setUp(self) -> None:
self.mock_flumine = mock.Mock()
with open("tests/resources/bbc663ed-064b-4237-a0ed-0c6e49fdef46.json") as f:
self.mock_market_book = json.load(f)
self.market = ToteMarket(
self.mock_flumine,
"bbc663ed-064b-4237-a0ed-0c6e49fdef46",
self.mock_market_book,
)

def test_call(self):
mock_market_book = {}
self.market(mock_market_book)
self.assertEqual(self.market.market_book, mock_market_book)

def test_publish_time(self):
self.assertEqual(
self.market.publish_time, self.market.market_book["publish_time"]
)

def test_bet_delay(self):
self.assertEqual(self.market.bet_delay, 0.0)

def test_market_name(self):
self.assertEqual(self.market.market_name, "BRIGHTON RACE 1 - WIN")

def test_event_type_id(self):
self.assertIsNone(self.market.event_type_id)

def test_event_id(self):
self.assertEqual(self.market.event_id, 0)

def test_competition_id(self):
self.assertIsNone(self.market.competition_id)

def test_market_type(self):
self.assertEqual(self.market.market_type, "WIN")

def test_market_start_datetime(self):
self.assertEqual(
self.market.market_start_datetime,
datetime.datetime(2026, 9, 28, 12, 30, tzinfo=datetime.timezone.utc),
)

def test_event_name(self):
self.assertEqual(
self.market.event_name, "HORSERACING-BRIGHTON-GB-2026-09-28-1230"
)

def test_country_code(self):
self.assertIsNone(self.market.country_code)

def test_venue(self):
self.assertEqual(self.market.venue, "Brighton")

def test_race_type(self):
self.assertIsNone(self.market.race_type)

def test_status(self):
self.assertEqual(self.market.status, "OPEN")
Loading