Skip to content

Commit

Permalink
Merge 58bc6ce into cc04e15
Browse files Browse the repository at this point in the history
  • Loading branch information
liampauling committed Feb 24, 2021
2 parents cc04e15 + 58bc6ce commit 8668ca3
Show file tree
Hide file tree
Showing 16 changed files with 313 additions and 176 deletions.
12 changes: 12 additions & 0 deletions HISTORY.rst
Expand Up @@ -3,6 +3,18 @@
Release History
---------------

1.17.1 (2021-02-24)
+++++++++++++++++++

**Improvements**

- Current and total transactions available from client
- `blotter.strategy_selection_orders` func added (speed improvement on exposure calc)

**Bug Fixes**

- Refactor of client transaction control to correctly apply the 5000 limit

1.17.0 (2021-02-22)
+++++++++++++++++++

Expand Down
12 changes: 11 additions & 1 deletion docs/markets.md
Expand Up @@ -32,7 +32,17 @@ Within markets you have market objects which contains current up to date market
- `elapsed_seconds_closed` Seconds since market was closed (543.21)
- `market_start_datetime` Market scheduled start time

### Middleware
## Blotter

The blotter is a simple and fast class to hold all orders for a particular market.

### Functions

- `strategy_orders(strategy)` Returns all orders related to a strategy
- `strategy_selection_orders(strategy, selection_id, handicap)` Returns all orders related to a strategy selection
- `selection_exposure(strategy, lookup)` Returns strategy/selection exposure

## Middleware

It is common that you want to carry about analysis on a market before passing through to strategies, similar to Django's middleware design flumine allows middleware to be executed.

Expand Down
4 changes: 2 additions & 2 deletions docs/quickstart.md
Expand Up @@ -96,11 +96,11 @@ class ExampleStrategy(BaseStrategy):
This order will be validated through controls, stored in the blotter and sent straight to the execution thread pool for execution. It is also possible to batch orders into transactions as follows:

```python
with market.transaction as t:
with market.transaction() as t:
market.place_order(order) # executed immediately in separate transaction
t.place_order(order) # executed on transaction __exit__

with market.transaction as t:
with market.transaction() as t:
t.place_order(order)

t.execute() # above order executed
Expand Down
2 changes: 1 addition & 1 deletion flumine/__version__.py
@@ -1,6 +1,6 @@
__title__ = "flumine"
__description__ = "Betfair trading framework"
__url__ = "https://github.com/liampauling/flumine"
__version__ = "1.17.0"
__version__ = "1.17.1"
__author__ = "Liam Pauling"
__license__ = "MIT"
4 changes: 2 additions & 2 deletions flumine/baseflumine.py
Expand Up @@ -16,7 +16,7 @@
from .execution.betfairexecution import BetfairExecution
from .execution.simulatedexecution import SimulatedExecution
from .order.process import process_current_orders
from .controls.clientcontrols import BaseControl, MaxOrderCount
from .controls.clientcontrols import BaseControl, MaxTransactionCount
from .controls.tradingcontrols import OrderValidation, StrategyExposure
from .controls.loggingcontrols import LoggingControl
from . import config, utils
Expand Down Expand Up @@ -69,7 +69,7 @@ def __init__(self, client: BaseClient):
self.add_trading_control(OrderValidation)
self.add_trading_control(StrategyExposure)
# register default client controls (processed in order)
self.add_client_control(MaxOrderCount)
self.add_client_control(MaxTransactionCount)

# workers
self._workers = []
Expand Down
23 changes: 21 additions & 2 deletions flumine/clients/baseclient.py
Expand Up @@ -42,7 +42,6 @@ def __init__(
self.account_details = None
self.account_funds = None
self.commission_paid = 0
self.chargeable_transaction_count = 0

self.execution = None # set during flumine init
self.trading_controls = []
Expand All @@ -65,6 +64,25 @@ def add_execution(self, flumine) -> None:
elif self.EXCHANGE == ExchangeType.BETFAIR:
self.execution = flumine.betfair_execution

def add_transaction(self, count: int, failed: bool = False) -> None:
for control in self.trading_controls:
if hasattr(control, "add_transaction"):
control.add_transaction(count, failed)

@property
def current_transaction_count_total(self) -> Optional[int]:
# current hours total transaction count
for control in self.trading_controls:
if control.NAME == "MAX_TRANSACTION_COUNT":
return control.current_transaction_count_total

@property
def transaction_count_total(self) -> Optional[int]:
# total transaction count
for control in self.trading_controls:
if control.NAME == "MAX_TRANSACTION_COUNT":
return control.transaction_count_total

@property
def min_bet_size(self) -> Optional[float]:
raise NotImplementedError
Expand All @@ -83,7 +101,8 @@ def info(self) -> dict:
"id": self.id,
"exchange": self.EXCHANGE.value if self.EXCHANGE else None,
"betting_client": self.betting_client,
"chargeable_transaction_count": self.chargeable_transaction_count,
"current_transaction_count_total": self.current_transaction_count_total,
"transaction_count_total": self.transaction_count_total,
"trading_controls": self.trading_controls,
"order_stream": self.order_stream,
"best_price_execution": self.best_price_execution,
Expand Down
112 changes: 55 additions & 57 deletions flumine/controls/clientcontrols.py
@@ -1,7 +1,7 @@
import datetime
import logging
import threading
from typing import Optional
from betfairlightweight.metadata import transaction_limit

from ..order.orderpackage import BaseOrder, OrderPackageType
from . import BaseControl
Expand All @@ -10,106 +10,104 @@
logger = logging.getLogger(__name__)


class MaxOrderCount(BaseControl):
class MaxTransactionCount(BaseControl):

"""
Counts and limits orders based on max
order count.
Only prevents placeOrders if limit is
reached.
transaction count as per:
- https://www.betfair.com/aboutUs/Betfair.Charges/#TranCharges2
- 5000 transactions per hour
- `A ‘transaction’ shall include all bets placed and all failed transactions`
Counts are updated after an execution,
thread safe due to the execution pool.
"""

NAME = "MAX_ORDER_COUNT"
NAME = "MAX_TRANSACTION_COUNT"

def __init__(self, flumine, client: BaseClient):
super(MaxOrderCount, self).__init__(flumine)
super(MaxTransactionCount, self).__init__(flumine)
self.client = client
self.total = 0
self.place_requests = 0
self.cancel_requests = 0
self.update_requests = 0
self.replace_requests = 0
self._next_hour = None
# this hour
self.current_transaction_count = 0
self.current_failed_transaction_count = 0
# total since start
self.transaction_count = 0
self.failed_transaction_count = 0
# thread lock
self._lock = threading.Lock()

def add_transaction(self, count: int, failed: bool = False) -> None:
with self._lock:
if failed:
self.failed_transaction_count += count
self.current_failed_transaction_count += count
else:
self.transaction_count += count
self.current_transaction_count += count

def _validate(self, order: BaseOrder, package_type: OrderPackageType) -> None:
if self._next_hour is None:
self._set_next_hour()
self._check_hour()
self.total += 1
if package_type == OrderPackageType.PLACE:
self._check_transaction_count(1)
self.place_requests += 1
if not self.safe:
self._on_error(
order,
"Max Order Count has been reached ({0}) for current hour".format(
self.transaction_count
),
)
elif package_type == OrderPackageType.CANCEL:
self.cancel_requests += 1
elif package_type == OrderPackageType.UPDATE:
self.update_requests += 1
elif package_type == OrderPackageType.REPLACE:
self.replace_requests += 1
if not self.safe:
self._on_error(
order,
"Max Transaction Count has been reached ({0}) for current hour".format(
self.current_transaction_count_total
),
)

def _check_hour(self) -> None:
if datetime.datetime.utcnow() > self._next_hour:
if self._next_hour is None:
self._set_next_hour()
elif datetime.datetime.utcnow() > self._next_hour:
logger.info(
"Execution new hour",
extra={
"transaction_count": self.transaction_count,
"place_requests": self.place_requests,
"cancel_requests": self.cancel_requests,
"update_requests": self.update_requests,
"replace_requests": self.replace_requests,
"current_transaction_count_total": self.current_transaction_count_total,
"current_transaction_count": self.current_transaction_count,
"current_failed_transaction_count": self.current_failed_transaction_count,
"total_transaction_count": self.transaction_count,
"total_failed_transaction_count": self.failed_transaction_count,
"client": self.client,
},
)
self._set_next_hour()
if self.transaction_count > transaction_limit:
self.client.chargeable_transaction_count += (
self.transaction_count - transaction_limit
)
self.transaction_count = 0

def _check_transaction_count(self, transaction_count: int) -> None:
self.transaction_count += transaction_count
if self.transaction_limit and self.transaction_count > self.transaction_limit:
logger.error(
"Transaction limit reached",
extra={
"transaction_count": self.transaction_count,
"transaction_limit": self.transaction_limit,
"client": self.client,
},
)

def _set_next_hour(self) -> None:
now = datetime.datetime.utcnow()
self._next_hour = (now + datetime.timedelta(hours=1)).replace(
minute=0, second=0, microsecond=0
)
self.current_transaction_count = 0
self.current_failed_transaction_count = 0

@property
def safe(self) -> bool:
self._check_hour()
if self.transaction_limit is None:
return True
elif self.transaction_count < self.transaction_limit:
elif self.current_transaction_count_total <= self.transaction_limit:
return True
else:
logger.error(
"Transaction limit reached",
extra={
"transaction_count": self.transaction_count,
"current_transaction_count_total": self.current_transaction_count_total,
"current_transaction_count": self.current_transaction_count,
"current_failed_transaction_count": self.current_failed_transaction_count,
"transaction_limit": self.transaction_limit,
"client": self.client,
},
)
return False

@property
def current_transaction_count_total(self) -> int:
return self.current_transaction_count + self.current_failed_transaction_count

@property
def transaction_count_total(self) -> int:
return self.transaction_count + self.failed_transaction_count

@property
def transaction_limit(self) -> Optional[int]:
return self.client.transaction_limit
28 changes: 28 additions & 0 deletions flumine/execution/betfairexecution.py
Expand Up @@ -35,6 +35,9 @@ def execute_place(
# https://docs.developer.betfair.com/display/1smk3cen4v3lu3yomq5qye0ni/Betting+Enums#BettingEnums-ExecutionReportStatus
pass

# update transaction counts
order_package.client.add_transaction(len(order_package))

def place(self, order_package: OrderPackageType, session: requests.Session):
return order_package.client.betting_client.betting.place_orders(
market_id=order_package.market_id,
Expand All @@ -51,6 +54,7 @@ def execute_cancel(
) -> None:
response = self._execution_helper(self.cancel, order_package, http_session)
if response:
failed_transaction_count = 0
order_lookup = {o.bet_id: o for o in order_package}
for instruction_report in response.cancel_instruction_reports:
# get order (can't rely on the order they are returned)
Expand All @@ -68,6 +72,7 @@ def execute_cancel(
order.executable()
elif instruction_report.status == "FAILURE":
order.executable()
failed_transaction_count += 1
elif instruction_report.status == "TIMEOUT":
order.executable()

Expand All @@ -76,6 +81,12 @@ def execute_cancel(
with order.trade:
order.executable()

# update transaction counts
if failed_transaction_count:
order_package.client.add_transaction(
failed_transaction_count, failed=True
)

def cancel(self, order_package: OrderPackageType, session: requests.Session):
# temp copy to prevent an empty list of instructions sent
# this can occur if order is matched during the execution
Expand All @@ -96,6 +107,7 @@ def execute_update(
) -> None:
response = self._execution_helper(self.update, order_package, http_session)
if response:
failed_transaction_count = 0
for (order, instruction_report) in zip(
order_package, response.update_instruction_reports
):
Expand All @@ -107,9 +119,16 @@ def execute_update(
order.executable()
elif instruction_report.status == "FAILURE":
order.executable()
failed_transaction_count += 1
elif instruction_report.status == "TIMEOUT":
order.executable()

# update transaction counts
if failed_transaction_count:
order_package.client.add_transaction(
failed_transaction_count, failed=True
)

def update(self, order_package: OrderPackageType, session: requests.Session):
return order_package.client.betting_client.betting.update_orders(
market_id=order_package.market_id,
Expand All @@ -123,6 +142,7 @@ def execute_replace(
) -> None:
response = self._execution_helper(self.replace, order_package, http_session)
if response:
failed_transaction_count = 0
market = self.flumine.markets.markets[order_package.market_id]
for (order, instruction_report) in zip(
order_package, response.replace_instruction_reports
Expand All @@ -144,6 +164,7 @@ def execute_replace(
== "FAILURE"
):
order.executable()
failed_transaction_count += 1
elif (
instruction_report.cancel_instruction_reports.status
== "TIMEOUT"
Expand Down Expand Up @@ -175,6 +196,13 @@ def execute_replace(
):
pass # todo

# update transaction counts
order_package.client.add_transaction(len(order_package))
if failed_transaction_count:
order_package.client.add_transaction(
failed_transaction_count, failed=True
)

def replace(self, order_package: OrderPackageType, session: requests.Session):
return order_package.client.betting_client.betting.replace_orders(
market_id=order_package.market_id,
Expand Down

0 comments on commit 8668ca3

Please sign in to comment.