Skip to content

Commit

Permalink
Merge pull request #375 from liampauling/task/strategy-selection-orders
Browse files Browse the repository at this point in the history
strategy selection orders lookup added and integrated (much faster lo…
  • Loading branch information
liampauling committed Feb 24, 2021
2 parents 8a79aa1 + 029296f commit 58bc6ce
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 45 deletions.
12 changes: 11 additions & 1 deletion docs/markets.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
4 changes: 2 additions & 2 deletions flumine/execution/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ class Transaction:
when it is used as a context manager requests can
be batched, for example:
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
70 changes: 38 additions & 32 deletions flumine/markets/blotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,21 @@ class Blotter:
def __init__(self, market_id: str):
self.market_id = market_id
self._orders = {} # {Order.id: Order}
self._live_orders = [] # cached list of live orders
self._strategy_orders = defaultdict(
list
) # cache list per strategy (faster lookup)
# cached lists/dicts for faster lookup
self._live_orders = []
self._strategy_orders = defaultdict(list)
self._strategy_selection_orders = defaultdict(list)

def strategy_orders(self, strategy) -> list:
"""Returns all orders related to a strategy."""
return self._strategy_orders[strategy]

def strategy_selection_orders(
self, strategy, selection_id: int, handicap: float = 0
) -> list:
"""Returns all orders related to a strategy selection."""
return self._strategy_selection_orders[(strategy, selection_id, handicap)]

@property
def live_orders(self):
return iter(self._live_orders)
Expand Down Expand Up @@ -75,37 +81,34 @@ def selection_exposure(self, strategy, lookup: tuple) -> float:
ub, ul = [], [] # unmatched bets, (price, size)
moc_win_liability = 0.0
moc_lose_liability = 0.0
for order in self.strategy_orders(strategy):
if order.lookup == lookup:
if order.status == OrderStatus.VIOLATION:
continue

if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
_size_matched = order.size_matched # cache
if _size_matched:
if order.side == "BACK":
mb.append((order.average_price_matched, _size_matched))
else:
ml.append((order.average_price_matched, _size_matched))
_size_remaining = order.size_remaining # cache
if order.order_type.price and _size_remaining:
if order.side == "BACK":
ub.append((order.order_type.price, _size_remaining))
else:
ul.append((order.order_type.price, _size_remaining))
elif order.order_type.ORDER_TYPE in (
OrderTypes.LIMIT_ON_CLOSE,
OrderTypes.MARKET_ON_CLOSE,
):
for order in self.strategy_selection_orders(strategy, *lookup[1:]):
if order.status == OrderStatus.VIOLATION:
continue
if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
_size_matched = order.size_matched # cache
if _size_matched:
if order.side == "BACK":
moc_lose_liability -= order.order_type.liability
mb.append((order.average_price_matched, _size_matched))
else:
moc_win_liability -= order.order_type.liability
ml.append((order.average_price_matched, _size_matched))
_size_remaining = order.size_remaining # cache
if order.order_type.price and _size_remaining:
if order.side == "BACK":
ub.append((order.order_type.price, _size_remaining))
else:
ul.append((order.order_type.price, _size_remaining))
elif order.order_type.ORDER_TYPE in (
OrderTypes.LIMIT_ON_CLOSE,
OrderTypes.MARKET_ON_CLOSE,
):
if order.side == "BACK":
moc_lose_liability -= order.order_type.liability
else:
raise ValueError(
"Unexpected order type: %s" % order.order_type.ORDER_TYPE
)

moc_win_liability -= order.order_type.liability
else:
raise ValueError(
"Unexpected order type: %s" % order.order_type.ORDER_TYPE
)
matched_exposure = calculate_matched_exposure(mb, ml)
unmatched_exposure = calculate_unmatched_exposure(ub, ul)

Expand All @@ -132,6 +135,9 @@ def __setitem__(self, customer_order_ref: str, order) -> None:
self._orders[customer_order_ref] = order
self._live_orders.append(order)
self._strategy_orders[order.trade.strategy].append(order)
self._strategy_selection_orders[
(order.trade.strategy, *order.lookup[1:])
].append(order)

def __getitem__(self, customer_order_ref: str):
return self._orders[customer_order_ref]
Expand Down
17 changes: 15 additions & 2 deletions tests/test_blotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@ def test_init(self):
self.assertEqual(self.blotter.market_id, "1.23")
self.assertEqual(self.blotter._orders, {})
self.assertEqual(self.blotter._live_orders, [])
self.assertEqual(self.blotter._strategy_orders, {})
self.assertEqual(self.blotter._strategy_selection_orders, {})

def test_strategy_orders(self):
mock_order = mock.Mock()
mock_order = mock.Mock(lookup=(1, 2, 3))
mock_order.trade.strategy = 69
self.blotter["12345"] = mock_order
self.assertEqual(self.blotter.strategy_orders(12), [])
self.assertEqual(self.blotter.strategy_orders(69), [mock_order])

def test_strategy_selection_orders(self):
mock_order = mock.Mock(lookup=(1, 2, 3))
mock_order.trade.strategy = 69
self.blotter["12345"] = mock_order
self.assertEqual(self.blotter.strategy_selection_orders(12, 2, 3), [])
self.assertEqual(self.blotter.strategy_selection_orders(69, 2, 3), [mock_order])

def test_live_orders(self):
self.assertEqual(list(self.blotter.live_orders), [])
mock_order = mock.Mock(complete=False)
Expand Down Expand Up @@ -247,13 +256,17 @@ def test__contains(self):
self.assertNotIn("321", self.blotter)

def test__setitem(self):
mock_order = mock.Mock()
mock_order = mock.Mock(lookup=(1, 2, 3))
self.blotter["123"] = mock_order
self.assertEqual(self.blotter._orders, {"123": mock_order})
self.assertEqual(self.blotter._live_orders, [mock_order])
self.assertEqual(
self.blotter._strategy_orders, {mock_order.trade.strategy: [mock_order]}
)
self.assertEqual(
self.blotter._strategy_selection_orders,
{(mock_order.trade.strategy, 2, 3): [mock_order]},
)

def test__getitem(self):
self.blotter._orders = {"12345": "test", "54321": "test2"}
Expand Down
8 changes: 2 additions & 6 deletions tests/test_tradingcontrols.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,10 @@ def test_validate_limit_with_multiple_strategies_succeeds(self, mock_on_error):
order1.order_type.price = 2.0
order1.order_type.size = 9.0
order1.size_remaining = 9.0
order1.lookup = "lookup"
order1.average_price_matched = 0.0
order1.size_matched = 0

order2 = mock.Mock()
order2 = mock.Mock(lookup=(1, 2, 3))
order2.trade.strategy.max_order_exposure = 10
order2.trade.strategy.max_selection_exposure = 10
order2.trade.strategy = strategy
Expand All @@ -344,7 +343,6 @@ def test_validate_limit_with_multiple_strategies_succeeds(self, mock_on_error):
order2.order_type.price = 3.0
order2.order_type.size = 9.0
order2.size_remaining = 5.0
order2.lookup = "lookup"
order2.average_price_matched = 0.0
order2.size_matched = 0

Expand All @@ -370,18 +368,16 @@ def test_validate_limit_with_multiple_strategies_fails(self, mock_on_error):
order1.order_type.price = 2.0
order1.order_type.size = 9.0
order1.size_remaining = 9.0
order1.lookup = "lookup"
order1.average_price_matched = 0.0
order1.size_matched = 0

order2 = mock.Mock()
order2 = mock.Mock(lookup=(1, 2, 3))
order2.trade.strategy = strategy
order2.order_type.ORDER_TYPE = OrderTypes.LIMIT
order2.side = "BACK"
order2.order_type.price = 3.0
order2.order_type.size = 9.0
order2.size_remaining = 5.0
order2.lookup = "lookup"
order2.average_price_matched = 0.0
order2.size_matched = 0

Expand Down

0 comments on commit 58bc6ce

Please sign in to comment.