Example #1
0
class Book:
    def __init__(self, symbol: str):
        self.asks = SortedDict()
        self.bids = SortedDict()

    def update(self, *entries: Iterable[dict]) -> None:
        for entry in entries:
            if entry['side'] == 'BID':
                self.bids[entry['price']] = entry
            else:
                self.asks[entry['price']] = entry

    def remove(self, side: str, price: float) -> None:
        if side == 'BID':
            self.bids.pop(price, None)
        else:
            self.asks.pop(price, None)

    def get_bids(self, size=-1) -> List[dict]:
        if size == -1 or size >= len(self.bids):
            return list(map(add_key, enumerate(reversed(self.bids.values()))))
        result = list(self.bids.values())[len(self.bids) - size:]
        result.reverse()
        return list(map(add_key, enumerate(result)))

    def get_asks(self, size=-1) -> List[dict]:
        if size == -1 or size >= len(self.asks):
            return list(map(add_key, enumerate(self.asks.values())))
        return list(map(add_key, enumerate(self.asks.values()[:size])))

    def clear(self) -> None:
        self.asks.clear()
        self.bids.clear()
Example #2
0
class OrderBook(object):
    def __init__(self, log_enabled=False):
        self.by_price = SortedDict()
        self.by_order_id = {}
        self.best_bid_level = None
        self.best_ask_level = None
        self.log_enabled = log_enabled
        self.logger = logging.getLogger(__name__)

    def _add(self, e):
        if e["next_microtimestamp"] != '-infinity':
            if self.log_enabled:
                self.logger.debug('Add %s', event_log(e))

            price_level = self.by_price.get(e["price"])
            if price_level is None:
                price_level = PriceLevel(e["price"])
                self.by_price[e["price"]] = price_level
            price_level.add(e)
            self.by_order_id[e["order_id"]] = e
            self.changed_price_levels[e["price"]] = price_level

            if e["side"] == 'b':
                if self.best_bid_level is None or\
                        e["price"] > self.best_bid_level.price:
                    self.best_bid_level = price_level
            else:
                if self.best_ask_level is None or\
                        e["price"] < self.best_ask_level.price:
                    self.best_ask_level = price_level

            if self.log_enabled:
                self.logger.debug('PL-add: %s',
                                  price_level_log(e["price"], price_level))

            return True
        else:
            if self.log_enabled:
                self.logger.debug('Ign %s', event_log(e))
            return False

    def _remove(self, order_id):
        old_event = self.by_order_id.pop(order_id, None)
        if old_event is not None:
            if self.log_enabled:
                self.logger.debug('Del %s', event_log(old_event))
            old_price_level = self.by_price[old_event["price"]]
            old_price_level.remove(old_event)
            if self.log_enabled:
                self.logger.debug(
                    'PL-del: %s',
                    price_level_log(old_event["price"], old_price_level))
            self.changed_price_levels[old_event["price"]] = old_price_level
            if old_event["side"] == 'b' and\
                    self.best_bid_level == old_price_level and\
                    old_price_level.amount('b') == Decimal(0):
                self.best_bid_level = None
                new_best_bid_level = old_price_level
                i = self.by_price.bisect_left(new_best_bid_level.price)
                while i > 0:
                    new_best_bid_level = self.by_price.values()[i - 1]
                    if new_best_bid_level.amount('b') > Decimal(0):
                        self.best_bid_level = new_best_bid_level
                        break
                    else:
                        i = self.by_price.bisect_left(new_best_bid_level.price)
            elif old_event["side"] == 's' and\
                    self.best_ask_level == old_price_level and\
                    old_price_level.amount('s') == Decimal(0):
                self.best_ask_level = None
                i = self.by_price.bisect_right(old_price_level.price)
                while i < len(self.by_price):
                    new_best_ask_level = self.by_price.values()[i]
                    if new_best_ask_level.amount('s') > Decimal(0):
                        self.best_ask_level = new_best_ask_level
                        break
                    else:
                        i = self.by_price.bisect_right(
                            new_best_ask_level.price)

        return old_event

    def _post_update(self):
        depth_changes = []
        for price in self.changed_price_levels.keys():
            price_level = self.changed_price_levels[price]
            if self.log_enabled:
                self.logger.debug('Changed PL: %s',
                                  price_level_log(price, price_level))
            for side in ['b', 's']:
                amount = price_level.amount(side)
                if amount >= Decimal(0):
                    # obanalytics.level2_depth_record
                    depth_changes.append({
                        "price": price,
                        "volume": amount,
                        "side": side,
                        "bps_level": None
                    })
            price_level.purge()
            if price_level.amount("s") is None and\
                    price_level.amount("b") is None:
                del self.by_price[price]
        return depth_changes

    def update(self, episode):
        if self.log_enabled:
            self.logger.debug('OB update %s starts',
                              episode[0]["microtimestamp"])
        episode = sorted(episode, key=lambda e: e["event_no"])
        self.changed_price_levels = SortedDict()
        for e in episode:
            if e["event_no"] > 1:
                self._remove(e["order_id"])
                if self.log_enabled:
                    self.logger.debug(spread_log(self))
            self._add(e)
            if self.log_enabled:
                self.logger.debug(spread_log(self))

        depth_changes = self._post_update()

        if self.log_enabled:
            self.logger.debug('OB update %s ends',
                              episode[0]["microtimestamp"])
        return depth_changes

    def event(self, order_id):
        return self.by_order_id.get(order_id)

    def spread(self):
        spread = {}
        if self.best_bid_level is not None:
            spread["best_bid_price"] = self.best_bid_level.price
            spread["best_bid_qty"] = self.best_bid_level.amount('b')
        else:
            spread["best_bid_price"] = None
            spread["best_bid_qty"] = None
        if self.best_ask_level is not None:
            spread["best_ask_price"] = self.best_ask_level.price
            spread["best_ask_qty"] = self.best_ask_level.amount('s')
        else:
            spread["best_ask_price"] = None
            spread["best_ask_qty"] = None
        return spread

    def events(self, price):
        price_level = self.by_price.get(price)
        if price_level is not None:
            return price_level.events()
        else:
            return None

    def all_events(self):
        best_bid_price = None
        best_sell_price = None
        for price in self.by_price:
            for e in self.by_price[price].events():
                if e["side"] == 'b':
                    if best_bid_price is None or e["price"] > best_bid_price:
                        best_bid_price = e["price"]
                    if best_sell_price is None:
                        e["is_maker"] = True
                        e["is_crossed"] = False
                    elif e["price"] <= best_sell_price:
                        e["is_maker"] = True
                        if e["price"] > best_sell_price:
                            e["is_crossed"] = True
                        else:
                            e["is_crossed"] = False
                    else:
                        e["is_maker"] = False
                        e["is_crossed"] = True
                else:
                    if best_sell_price is None or e["price"] < best_sell_price:
                        best_sell_price = e["price"]
                    if best_bid_price is None:
                        e["is_maker"] = True
                        e["is_crossed"] = False
                    elif e["price"] >= best_bid_price:
                        e["is_maker"] = True
                        if e["price"] < best_bid_price:
                            e["is_crossed"] = True
                        else:
                            e["is_crossed"] = False
                    else:
                        e["is_maker"] = False
                        e["is_crossed"] = True
                yield e
Example #3
0
class TimingDiagram:
    """Two-state (True/False or 1/0) timing diagram with boolean algebra operations."""

    def __init__(self, time_state_pairs):
        """Creates a timing diagram out of a series of (time, state) pairs.

        Notes
        =====
        The input states can be any truthy/falsey values.
        The input times can be any type with a partial ordering.
        The input sequence does not need to be sorted (input is sorted during initialization).
        Compresses duplicate sequential states and stores them in the `timeline` attribute.

        Example
        =======
        >>> diagram = TimingDiagram([(0, True), (1, False), (5, False), (10, True)])
        >>> print(~diagram)
        TimingDiagram([(0, False), (1, True), (10, False)])
        """
        self.timeline = SortedDict(
            _compress(time_state_pairs, key=operator.itemgetter(1))
        )

    def __getitem__(self, item):
        return self.timeline[item]

    def __matmul__(self, time):
        """Alias for at()"""
        return self.at(time)

    def __eq__(self, other):
        """Returns a new timing diagram, True where the two diagrams are equal."""
        return self.compare(other, key=operator.eq)

    def __ne__(self, other):
        """Returns a new timing diagram, True where the two diagrams are equal."""
        return ~(self == other)

    def __and__(self, other):
        """Returns a new timing diagram, True where the two diagrams are both True."""
        return self.compare(other, key=operator.and_)

    def __or__(self, other):
        """Returns a new timing diagram, True where either diagram is True."""
        return self.compare(other, key=operator.or_)

    def __xor__(self, other):
        """Returns a new timing diagram, True where the two diagrams are not equal."""
        return self != other

    def __invert__(self):
        """Returns a new timing diagram with states flipped."""
        return TimingDiagram(((t, not s) for t, s in self.timeline.items()))

    def at(self, time):
        """Returns the state at a particular time. Uses bisection for search (binary search)."""
        idx = max(0, self.timeline.bisect(time) - 1)
        return self.timeline.values()[idx]

    def compare(self, other, key):
        """Constructs a new timing diagram based on comparisons between two diagrams,
        with (time, key(self[time], other[time])) for each time in the timelines.
        """
        # TODO: Implement linear algorithm instead of .at() for each time, which is O(n log n).
        return TimingDiagram(
            (
                (k, key(self.at(k), other.at(k)))
                for k in merge(self.timeline.keys(), other.timeline.keys())
            )
        )

    def __repr__(self):
        return f"{self.__class__.__qualname__}({list(self.timeline.items())})"