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()
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
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())})"