-
Notifications
You must be signed in to change notification settings - Fork 856
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
53fe7e0
commit d6b7f68
Showing
7 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .depth_infinite import InfiniteDepthOrderBook # noqa | ||
from .depth_finite import FiniteDepthOrderBook # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from abc import ABCMeta, abstractmethod | ||
|
||
|
||
class AbstractOrderBook(object): | ||
""" | ||
The AbstractOrderBook abstract class | ||
to manage orderbook | ||
""" | ||
|
||
__metaclass__ = ABCMeta | ||
|
||
def price(self, volume=0): | ||
if volume > 0: | ||
return self.ask(volume) | ||
elif volume < 0: | ||
return self.bid(-volume) | ||
else: # volume==0 | ||
return self.midpoint(volume) | ||
|
||
def midpoint(self, volume=0): | ||
return (self.bid(volume) + self.ask(volume)) // 2 | ||
|
||
@abstractmethod | ||
def bid(self, volume=0): | ||
""" | ||
Return bid price for a given volume | ||
if no volume is given, highest bid is returned | ||
""" | ||
raise NotImplementedError("Should implement bid(...)") | ||
|
||
@abstractmethod | ||
def ask(self, volume=0): | ||
""" | ||
Return ask price for a given volume | ||
if no volume is given, lowest ask is returned | ||
""" | ||
raise NotImplementedError("Should implement ask(...)") | ||
|
||
def spread(self, volume=0): | ||
""" | ||
Return spread for a given volume | ||
""" | ||
return self.ask(volume) - self.bid(volume) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from .base import AbstractOrderBook | ||
from .exceptions import OrderBookConstructionException, OrderBookLiquidityException | ||
|
||
|
||
def issorted(lst, reverse=False): | ||
return sorted(lst, reverse=reverse) == lst | ||
|
||
|
||
def isunique(lst): | ||
s = set() | ||
for lv in lst: | ||
s.add(lv.price) | ||
return len(s) == len(lst) | ||
|
||
|
||
class FiniteDepthOrderBook(AbstractOrderBook): | ||
def __init__(self, asks, bids): | ||
if not issorted(asks): | ||
raise OrderBookConstructionException("asks must have price ascending") | ||
|
||
if not issorted(bids, reverse=True): | ||
raise OrderBookConstructionException("bids must have price descending") | ||
|
||
if not isunique(bids): | ||
raise OrderBookConstructionException("bids must have unique prices") | ||
|
||
if not isunique(asks): | ||
raise OrderBookConstructionException("asks must have unique prices") | ||
|
||
self._asks = asks | ||
self._bids = bids | ||
|
||
def _price(self, v_level, volume): | ||
assert volume >= 0, "volume must be positive or zero" | ||
remaining_volume = volume | ||
price_volume_sum = 0 | ||
for level in v_level: | ||
if level.volume >= remaining_volume: | ||
taken_volume = remaining_volume | ||
remaining_volume = 0 | ||
price_volume_sum += (level.price * taken_volume) | ||
break | ||
else: | ||
taken_volume = level.volume | ||
remaining_volume -= level.volume | ||
price_volume_sum += (level.price * level.volume) | ||
total_volume = volume - remaining_volume | ||
price = price_volume_sum / total_volume | ||
if remaining_volume != 0: | ||
raise OrderBookLiquidityException("Orderbook doesn't have enough depth") | ||
return price | ||
|
||
def bid(self, volume=0): | ||
if volume == 0: | ||
return self._bids[0].price | ||
else: | ||
return self._price(self._bids, volume) | ||
|
||
def ask(self, volume=0): | ||
if volume == 0: | ||
return self._asks[0].price | ||
else: | ||
return self._price(self._asks, volume) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from .base import AbstractOrderBook | ||
from .exceptions import OrderBookConstructionException, OrderBookLiquidityException | ||
|
||
|
||
class InfiniteDepthOrderBook(AbstractOrderBook): | ||
def __init__(self, lowest_ask, highest_bid=None, volume=None): | ||
if highest_bid is None: | ||
highest_bid = lowest_ask | ||
if lowest_ask < highest_bid: | ||
raise OrderBookConstructionException("lowest_ask must be greater than or equal to highest_bid") | ||
|
||
self._highest_bid = highest_bid | ||
self._lowest_ask = lowest_ask | ||
self._volume = volume # maximum volume | ||
|
||
def _test_liquidity(self, volume): | ||
if self._volume is not None: | ||
if volume > self._volume: | ||
raise OrderBookLiquidityException("illiquid orderbook") | ||
|
||
def bid(self, volume=0): | ||
""" | ||
Return highest bid | ||
""" | ||
self._test_liquidity(volume) | ||
return self._highest_bid | ||
|
||
def ask(self, volume=0): | ||
""" | ||
Return lowest ask | ||
""" | ||
self._test_liquidity(volume) | ||
return self._lowest_ask |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class OrderBookException(Exception): | ||
pass | ||
|
||
|
||
class OrderBookConstructionException(Exception): | ||
pass | ||
|
||
|
||
class OrderBookLiquidityException(Exception): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class Level(object): | ||
def __init__(self, price, volume): | ||
self.price = price | ||
self.volume = volume | ||
|
||
def __gt__(self, other): | ||
return self.price > other.price | ||
|
||
def __repr__(self): | ||
return "%s:%s" % (self.price, self.volume) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import unittest | ||
|
||
from qstrader.orderbook import InfiniteDepthOrderBook, FiniteDepthOrderBook | ||
from qstrader.orderbook.exceptions import OrderBookConstructionException, OrderBookLiquidityException | ||
from qstrader.orderbook.level import Level | ||
|
||
|
||
class TestInfiniteDepthOrderBook(unittest.TestCase): | ||
""" | ||
Test an orderbook with infinite depth | ||
""" | ||
def setUp(self): | ||
pass | ||
|
||
def test_orderbook_with_bid_ask(self): | ||
_ask, _bid = 110.0, 100.0 | ||
self.ob = InfiniteDepthOrderBook(_ask, _bid) | ||
|
||
self.assertEqual(self.ob.spread(), 10.0) | ||
self.assertEqual(self.ob.bid(), 100.0) | ||
self.assertEqual(self.ob.ask(), 110.0) | ||
volume = 50 | ||
self.assertEqual(self.ob.bid(volume), 100.0) | ||
self.assertEqual(self.ob.ask(volume), 110.0) | ||
|
||
def test_orderbook_with_only_one_price(self): | ||
price = 100.0 | ||
self.ob = InfiniteDepthOrderBook(price) | ||
self.assertEqual(self.ob.bid(), price) | ||
self.assertEqual(self.ob.ask(), price) | ||
self.assertEqual(self.ob.spread(), 0.0) | ||
volume = 50 | ||
self.assertEqual(self.ob.bid(volume), price) | ||
self.assertEqual(self.ob.ask(volume), price) | ||
self.assertEqual(self.ob.spread(volume), 0.0) | ||
|
||
def test_orderbook_construction_error(self): | ||
_ask, _bid = 100.0, 110.0 # NOT ask >= bid | ||
self.assertRaises(OrderBookConstructionException, InfiniteDepthOrderBook, _ask, _bid) | ||
|
||
def test_illiquid_orderbook(self): | ||
_ask, _bid = 110.0, 100.0 | ||
_vol = 100 | ||
self.ob = InfiniteDepthOrderBook(_ask, _bid, _vol) | ||
self.assertRaises(OrderBookLiquidityException, self.ob.price, _vol + 10) | ||
|
||
|
||
class TestLevel(unittest.TestCase): | ||
def test_level(self): | ||
l1 = Level(1.34, 100.2) | ||
l2 = Level(1.35, 110.2) | ||
self.assertTrue(l1 < l2) | ||
|
||
|
||
class TestFiniteDepthOrderBook(unittest.TestCase): | ||
""" | ||
Test an orderbook with finite depth | ||
""" | ||
def setUp(self): | ||
pass | ||
|
||
def test_low_volume(self): | ||
asks = [Level(110.0, 10.0), Level(111.0, 12.0)] # asks (ascending asks) | ||
bids = [Level(100.0, 10.0), Level(99.0, 15.0)] # bids (descending prices) | ||
|
||
ob = FiniteDepthOrderBook(asks, bids) | ||
self.assertEqual(ob.bid(), 100.0) | ||
self.assertEqual(ob.ask(), 110.0) | ||
self.assertEqual(ob.spread(), 10.0) | ||
|
||
volume = 15 | ||
expected_bid = (100.0 * 10.0 + 99.0 * 5.0) / 15.0 | ||
calc_bid = ob.bid(volume) | ||
self.assertEqual(calc_bid, expected_bid) | ||
|
||
expected_ask = (110.0 * 10.0 + 111 * 5.0) / 15.0 | ||
calc_ask = ob.ask(volume) | ||
self.assertEqual(calc_ask, expected_ask) | ||
|
||
self.assertEqual(ob.spread(volume), expected_ask - expected_bid) | ||
|
||
self.assertEqual(ob.price(volume), ob.ask(volume)) | ||
self.assertEqual(ob.price(-volume), ob.bid(volume)) | ||
|
||
def test_illiquid_orderbook(self): | ||
asks = [Level(110.0, 10.0), Level(111.0, 12.0)] # asks (ascending asks) | ||
bids = [Level(100.0, 10.0), Level(99.0, 15.0)] # bids (descending prices) | ||
|
||
ob = FiniteDepthOrderBook(asks, bids) | ||
|
||
volume = 50.0 | ||
self.assertRaises(OrderBookLiquidityException, ob.spread, volume) | ||
|
||
def test_construction_error_not_ascending_asks(self): | ||
asks = [Level(111.0, 10.0), Level(110.0, 12.0)] # asks (NOT ascending asks) | ||
bids = [Level(100.0, 10.0), Level(99.0, 15.0)] # bids (descending prices) | ||
self.assertRaises(OrderBookConstructionException, FiniteDepthOrderBook, asks, bids) | ||
|
||
def test_construction_error_not_descending_bids(self): | ||
asks = [Level(110.0, 10.0), Level(111.0, 12.0)] # asks (ascending asks) | ||
bids = [Level(99.0, 10.0), Level(100.0, 15.0)] # bids (NOT descending prices) | ||
self.assertRaises(OrderBookConstructionException, FiniteDepthOrderBook, asks, bids) | ||
|
||
def test_construction_error_not_unique_asks(self): | ||
asks = [Level(111.0, 10.0), Level(111.0, 12.0)] # asks (NOT unique prices) | ||
bids = [Level(100.0, 10.0), Level(99.0, 15.0)] # bids (descending prices) | ||
self.assertRaises(OrderBookConstructionException, FiniteDepthOrderBook, asks, bids) | ||
|
||
def test_construction_error_not_unique_bids(self): | ||
asks = [Level(110.0, 10.0), Level(111.0, 12.0)] # asks (ascending asks) | ||
bids = [Level(100.0, 10.0), Level(100.0, 15.0)] # bids (NOT unique prices) | ||
self.assertRaises(OrderBookConstructionException, FiniteDepthOrderBook, asks, bids) |