Skip to content

Commit

Permalink
WIP: OrderBook
Browse files Browse the repository at this point in the history
  • Loading branch information
femtotrader committed Jan 26, 2017
1 parent 53fe7e0 commit 36ff82a
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 0 deletions.
2 changes: 2 additions & 0 deletions qstrader/orderbook/__init__.py
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
43 changes: 43 additions & 0 deletions qstrader/orderbook/base.py
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)
60 changes: 60 additions & 0 deletions qstrader/orderbook/depth_finite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from .base import AbstractOrderBook
from .exceptions import OrderBookConstructionException, OrderBookLiquidityException


def issorted(lst, reverse=False):
return sorted(lst, reverse=reverse) == lst


def isunique(lst):
return len(set(lst)) == 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) != bids:
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)
33 changes: 33 additions & 0 deletions qstrader/orderbook/depth_infinite.py
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
10 changes: 10 additions & 0 deletions qstrader/orderbook/exceptions.py
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
7 changes: 7 additions & 0 deletions qstrader/orderbook/level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Level(object):
def __init__(self, price, volume):
self.price = price
self.volume = volume

def __gt__(self, other):
return self.price > other.price
115 changes: 115 additions & 0 deletions tests/test_orderbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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):
pass
# 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):
pass
# 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):
pass
# 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)

0 comments on commit 36ff82a

Please sign in to comment.