diff --git a/Dockerfile b/Dockerfile index 0ebba5a..8c6ffde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.9-buster -RUN sh -c "$(curl -sSfL https://release.solana.com/v1.8.0/install)" +RUN sh -c "$(curl -sSfL https://release.solana.com/v1.8.1/install)" RUN apt-get update && apt-get -y install bc curl zlib1g-dev diff --git a/bin/show-orders b/bin/show-orders index 598c823..001d05c 100755 --- a/bin/show-orders +++ b/bin/show-orders @@ -26,13 +26,7 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args) group = mango.Group.load(context, context.group_address) account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address) -market_symbol = args.market.upper() -market = context.market_lookup.find_by_symbol(market_symbol) -if market is None: - raise Exception(f"Could not find market {market_symbol}") +market = mango.load_market_by_symbol(context, args.market) -market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run) -orders = market_operations.load_orders() -print(f"{len(orders)} order(s) to show.") -for order in orders: - print(order) +orderbook = market.fetch_orderbook(context) +print(orderbook) diff --git a/mango/__init__.py b/mango/__init__.py index 9403d2f..dcd2ab2 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -45,7 +45,7 @@ from .observables import DisposePropagator, DisposeWrapper, NullObserverSubscrib from .openorders import OpenOrders from .oracle import OracleSource, Price, Oracle, OracleProvider, SupportedOracleFeature from .orderbookside import OrderBookSideType, PerpOrderBookSide -from .orders import Order, OrderType, Side +from .orders import Order, OrderType, Side, OrderBook from .ownedtokenvalue import OwnedTokenValue from .oraclefactory import create_oracle_provider from .parse_account_info_to_orders import parse_account_info_to_orders @@ -83,7 +83,7 @@ from .version import Version from .wallet import Wallet from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, parse_target_balance, parse_fixed_target_balance, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer, LiveAccountBalancer from .watcher import Watcher, ManualUpdateWatcher, LamdaUpdateWatcher -from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_perp_orderbook_side_watcher, build_serum_orderbook_side_watcher +from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_orderbook_watcher from .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager from .layouts import layouts diff --git a/mango/account.py b/mango/account.py index df92581..7d51ed4 100644 --- a/mango/account.py +++ b/mango/account.py @@ -200,6 +200,28 @@ class Account(AddressableAccount): raise Exception(f"Account account not found at address '{address}'") return Account.parse(account_info, group) + @staticmethod + def load_all(context: Context, group: Group) -> typing.Sequence["Account"]: + # mango_group is just after the METADATA, which is the first entry. + group_offset = layouts.METADATA.sizeof() + # owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes. + filters = [ + MemcmpOpts( + offset=group_offset, + bytes=encode_key(group.address) + ) + ] + + results = context.client.get_program_accounts( + context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof()) + accounts = [] + for account_data in results: + address = PublicKey(account_data["pubkey"]) + account_info = AccountInfo._from_response_values(account_data["account"], address) + account = Account.parse(account_info, group) + accounts += [account] + return accounts + @staticmethod def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]: # mango_group is just after the METADATA, which is the first entry. @@ -217,7 +239,8 @@ class Account(AddressableAccount): ) ] - results = context.client.get_program_accounts(context.mango_program_address, memcmp_opts=filters) + results = context.client.get_program_accounts( + context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof()) accounts = [] for account_data in results: address = PublicKey(account_data["pubkey"]) diff --git a/mango/loadedmarket.py b/mango/loadedmarket.py index 7f8e587..a563b0f 100644 --- a/mango/loadedmarket.py +++ b/mango/loadedmarket.py @@ -13,15 +13,15 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) - import typing from solana.publickey import PublicKey +from .accountinfo import AccountInfo from .context import Context from .lotsizeconverter import LotSizeConverter from .market import Market, InventorySource -from .orders import Order +from .orders import Order, OrderBook from .token import Token @@ -33,5 +33,22 @@ class LoadedMarket(Market): def __init__(self, program_address: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token, lot_size_converter: LotSizeConverter): super().__init__(program_address, address, inventory_source, base, quote, lot_size_converter) - def orders(self, context: Context) -> typing.Sequence[Order]: - raise NotImplementedError("LoadedMarket.orders() is not implemented on the base type.") + @property + def bids_address(self) -> PublicKey: + raise NotImplementedError("LoadedMarket.bids_address() is not implemented on the base type.") + + @property + def asks_address(self) -> PublicKey: + raise NotImplementedError("LoadedMarket.asks_address() is not implemented on the base type.") + + def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: + raise NotImplementedError("LoadedMarket.parse_account_info_to_orders() is not implemented on the base type.") + + def parse_account_infos_to_orderbook(self, bids_account_info: AccountInfo, asks_account_info: AccountInfo) -> OrderBook: + bids_orderbook = self.parse_account_info_to_orders(bids_account_info) + asks_orderbook = self.parse_account_info_to_orders(asks_account_info) + return OrderBook(self.symbol, bids_orderbook, asks_orderbook) + + def fetch_orderbook(self, context: Context) -> OrderBook: + [bids_info, asks_info] = AccountInfo.load_multiple(context, [self.bids_address, self.asks_address]) + return self.parse_account_infos_to_orderbook(bids_info, asks_info) diff --git a/mango/marketmaking/modelstatebuilder.py b/mango/marketmaking/modelstatebuilder.py index 9fe0ef6..14a0058 100644 --- a/mango/marketmaking/modelstatebuilder.py +++ b/mango/marketmaking/modelstatebuilder.py @@ -87,19 +87,17 @@ class PollingModelStateBuilder(ModelStateBuilder): def from_values(self, order_owner: PublicKey, market: mango.Market, group: mango.Group, account: mango.Account, price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer, - inventory: mango.Inventory, bids: typing.Sequence[mango.Order], - asks: typing.Sequence[mango.Order]) -> ModelState: + inventory: mango.Inventory, orderbook: mango.OrderBook) -> ModelState: group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) placed_orders_container_watcher: mango.ManualUpdateWatcher[ mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) - bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids) - asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks) + orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook) return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher, - placed_orders_container_watcher, inventory_watcher, bids_watcher, asks_watcher) + placed_orders_container_watcher, inventory_watcher, orderbook_watcher) def __str__(self) -> str: return "Β« π™Ώπš˜πš•πš•πš’πš—πšπ™Όπš˜πšπšŽπš•πš‚πšπšŠπšπšŽπ™±πšžπš’πš•πšπšŽπš› Β»" @@ -138,8 +136,8 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): self.open_orders_address, self.base_inventory_token_account.address, self.quote_inventory_token_account.address, - self.market.underlying_serum_market.state.bids(), - self.market.underlying_serum_market.state.asks() + self.market.bids_address, + self.market.asks_address ] account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) group: mango.Group = mango.Group.parse(context, account_infos[0]) @@ -156,11 +154,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): quote_inventory_token_account = mango.TokenAccount.parse( account_infos[4], self.quote_inventory_token_account.value.token) - # Both these will have top-of-book at index 0. - bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[5], self.market.underlying_serum_market) - asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[6], self.market.underlying_serum_market) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[5], account_infos[6]) price: mango.Price = self.oracle.fetch_price(context) @@ -173,7 +167,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): base_inventory_token_account.value, quote_inventory_token_account.value) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids, asks) + return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook) def __str__(self) -> str: return f"""Β« πš‚πšŽπš›πšžπš–π™Ώπš˜πš•πš•πš’πš—πšπ™Όπš˜πšπšŽπš•πš‚πšπšŠπšπšŽπ™±πšžπš’πš•πšπšŽπš› for market '{self.market.symbol}' Β»""" @@ -212,8 +206,8 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.group_address, self.cache_address, self.account_address, - self.market.underlying_serum_market.state.bids(), - self.market.underlying_serum_market.state.asks(), + self.market.bids_address, + self.market.asks_address, *self.all_open_orders_addresses ] account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) @@ -254,15 +248,11 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): base_value, quote_value) - # Both these will have top-of-book at index 0. - bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[3], self.market.underlying_serum_market) - asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[4], self.market.underlying_serum_market) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4]) price: mango.Price = self.oracle.fetch_price(context) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids, asks) + return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook) def __str__(self) -> str: return f"""Β« πš‚πš™πš˜πšπ™Ώπš˜πš•πš•πš’πš—πšπ™Όπš˜πšπšŽπš•πš‚πšπšŠπšπšŽπ™±πšžπš’πš•πšπšŽπš› for market '{self.market.symbol}' Β»""" @@ -322,15 +312,11 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): base_token_value, quote_token_value) - # Both these will have top-of-book at index 0. - bids: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse( - context, account_infos[3], self.market.underlying_perp_market) - asks: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse( - context, account_infos[4], self.market.underlying_perp_market) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4]) price: mango.Price = self.oracle.fetch_price(context) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids.orders(), asks.orders()) + return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook) def __str__(self) -> str: return f"""Β« π™ΏπšŽπš›πš™π™Ώπš˜πš•πš•πš’πš—πšπ™Όπš˜πšπšŽπš•πš‚πšπšŠπšπšŽπ™±πšžπš’πš•πšπšŽπš› for market '{self.market.symbol}' Β»""" diff --git a/mango/marketmaking/modelstatebuilderfactory.py b/mango/marketmaking/modelstatebuilderfactory.py index 718d69d..0677dd8 100644 --- a/mango/marketmaking/modelstatebuilderfactory.py +++ b/mango/marketmaking/modelstatebuilderfactory.py @@ -129,10 +129,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man context, websocket_manager, health_check, disposer, wallet, market, price_watcher) latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher( context, websocket_manager, health_check, market, wallet) - latest_bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( - context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS) - latest_asks_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( - context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS) + latest_orderbook_watcher = mango.build_orderbook_watcher( + context, websocket_manager, health_check, market) elif isinstance(market, mango.SpotMarket): market_index: int = group.find_spot_market_index(market.address) order_owner = account.spot_open_orders[market_index] or SYSTEM_PROGRAM_ADDRESS @@ -157,10 +155,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man inventory_watcher = mango.SpotInventoryAccountWatcher( market, latest_account_observer, latest_group_observer, all_open_orders_watchers, cache_watcher) - latest_bids_watcher = mango.build_serum_orderbook_side_watcher( - context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS) - latest_asks_watcher = mango.build_serum_orderbook_side_watcher( - context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS) + latest_orderbook_watcher = mango.build_orderbook_watcher( + context, websocket_manager, health_check, market) elif isinstance(market, mango.PerpMarket): order_owner = account.address cache = mango.Cache.load(context, group.cache) @@ -169,14 +165,12 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man market, latest_account_observer, latest_group_observer, cache_watcher, group) latest_open_orders_observer = mango.build_perp_open_orders_watcher( context, websocket_manager, health_check, market, account, group, account_subscription) - latest_bids_watcher = mango.build_perp_orderbook_side_watcher( - context, websocket_manager, health_check, market, mango.OrderBookSideType.BIDS) - latest_asks_watcher = mango.build_perp_orderbook_side_watcher( - context, websocket_manager, health_check, market, mango.OrderBookSideType.ASKS) + latest_orderbook_watcher = mango.build_orderbook_watcher( + context, websocket_manager, health_check, market) else: raise Exception(f"Could not determine type of market {market.symbol}") model_state = ModelState(order_owner, market, latest_group_observer, latest_account_observer, latest_price_observer, latest_open_orders_observer, - inventory_watcher, latest_bids_watcher, latest_asks_watcher) + inventory_watcher, latest_orderbook_watcher) return WebsocketModelStateBuilder(model_state) diff --git a/mango/marketoperations.py b/mango/marketoperations.py index b72cf42..609ef24 100644 --- a/mango/marketoperations.py +++ b/mango/marketoperations.py @@ -23,7 +23,7 @@ from solana.publickey import PublicKey from .constants import SYSTEM_PROGRAM_ADDRESS from .market import Market, DryRunMarket -from .orders import Order +from .orders import Order, OrderBook # # πŸ₯­ MarketOperations @@ -65,7 +65,7 @@ class MarketOperations(metaclass=abc.ABCMeta): raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.") @abc.abstractmethod - def load_orders(self) -> typing.Sequence[Order]: + def load_orderbook(self) -> OrderBook: raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.") @abc.abstractmethod @@ -110,8 +110,8 @@ class DryRunMarketOperations(MarketOperations): self.logger.info(f"[Dry Run] Not placing order {order}.") return order - def load_orders(self) -> typing.Sequence[Order]: - return [] + def load_orderbook(self) -> OrderBook: + return OrderBook(self.market_name, [], []) def load_my_orders(self) -> typing.Sequence[Order]: return [] diff --git a/mango/modelstate.py b/mango/modelstate.py index 3011f06..b767f6b 100644 --- a/mango/modelstate.py +++ b/mango/modelstate.py @@ -25,7 +25,7 @@ from .group import Group from .inventory import Inventory from .market import Market from .oracle import Price -from .orders import Order +from .orders import Order, OrderBook from .placedorder import PlacedOrdersContainer from .watcher import Watcher @@ -43,8 +43,7 @@ class ModelState: price_watcher: Watcher[Price], placed_orders_container_watcher: Watcher[PlacedOrdersContainer], inventory_watcher: Watcher[Inventory], - bids: Watcher[typing.Sequence[Order]], - asks: Watcher[typing.Sequence[Order]] + orderbook: Watcher[OrderBook] ): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.order_owner: PublicKey = order_owner @@ -55,8 +54,7 @@ class ModelState: self.placed_orders_container_watcher: Watcher[ PlacedOrdersContainer] = placed_orders_container_watcher self.inventory_watcher: Watcher[Inventory] = inventory_watcher - self.bids_watcher: Watcher[typing.Sequence[Order]] = bids - self.asks_watcher: Watcher[typing.Sequence[Order]] = asks + self.orderbook_watcher: Watcher[OrderBook] = orderbook self.not_quoting: bool = False self.state: typing.Dict[str, typing.Any] = {} @@ -81,43 +79,35 @@ class ModelState: def inventory(self) -> Inventory: return self.inventory_watcher.latest + @property + def orderbook(self) -> OrderBook: + return self.orderbook_watcher.latest + @property def bids(self) -> typing.Sequence[Order]: - return self.bids_watcher.latest + return self.orderbook.bids @property def asks(self) -> typing.Sequence[Order]: - return self.asks_watcher.latest + return self.orderbook.asks # The top bid is the highest price someone is willing to pay to BUY @property def top_bid(self) -> typing.Optional[Order]: - if self.bids_watcher.latest and len(self.bids_watcher.latest) > 0: - # Top-of-book is always at index 0 for us. - return self.bids_watcher.latest[0] - else: - return None + return self.orderbook.top_bid # The top ask is the lowest price someone is willing to pay to SELL @property def top_ask(self) -> typing.Optional[Order]: - if self.asks_watcher.latest and len(self.asks_watcher.latest) > 0: - # Top-of-book is always at index 0 for us. - return self.asks_watcher.latest[0] - else: - return None + return self.orderbook.top_ask @property def spread(self) -> Decimal: - top_ask = self.top_ask - top_bid = self.top_bid - if top_ask is None or top_bid is None: - return Decimal(0) - else: - return top_ask.price - top_bid.price + return self.orderbook.spread def current_orders(self) -> typing.Sequence[Order]: - all_orders = [*self.bids_watcher.latest, *self.asks_watcher.latest] + self.orderbook + all_orders = [*self.bids, *self.asks] return list([o for o in all_orders if o.owner == self.order_owner]) def __str__(self) -> str: @@ -127,8 +117,8 @@ class ModelState: Price: {self.price_watcher.latest} Inventory: {self.inventory_watcher.latest} Existing Order Count: {len(self.placed_orders_container_watcher.latest.placed_orders)} - Bid Count: {len(self.bids_watcher.latest)} - Ask Count: {len(self.bids_watcher.latest)} + Bid Count: {len(self.bids)} + Ask Count: {len(self.asks)} Β»""" def __repr__(self) -> str: diff --git a/mango/oracles/market/market.py b/mango/oracles/market/market.py index 2935a9b..0a2893a 100644 --- a/mango/oracles/market/market.py +++ b/mango/oracles/market/market.py @@ -27,7 +27,7 @@ from ...loadedmarket import LoadedMarket from ...market import Market from ...observables import observable_pipeline_error_reporter from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature -from ...orders import Order, Side +from ...orders import OrderBook # # πŸ₯­ Market @@ -59,10 +59,18 @@ class MarketOracle(Oracle): self.source: OracleSource = OracleSource("Market", name, features, market) def fetch_price(self, context: Context) -> Price: - orders: typing.Sequence[Order] = self.loaded_market.orders(context) - top_bid = max([order.price for order in orders if order.side == Side.BUY]) - top_ask = min([order.price for order in orders if order.side == Side.SELL]) - mid_price = (top_bid + top_ask) / 2 + orderbook: OrderBook = self.loaded_market.fetch_orderbook(context) + if orderbook.top_bid is None: + raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid") + top_bid = orderbook.top_bid.price + + if orderbook.top_ask is None: + raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid") + top_ask = orderbook.top_ask.price + + if orderbook.mid_price is None: + raise Exception(f"[{self.source}] Cannot determine complete price data - no mid price") + mid_price = orderbook.mid_price return Price(self.source, datetime.now(), self.market, top_bid, mid_price, top_ask, MarketOracleConfidence) diff --git a/mango/orderbookside.py b/mango/orderbookside.py index 68ec857..256a8e2 100644 --- a/mango/orderbookside.py +++ b/mango/orderbookside.py @@ -80,7 +80,7 @@ class PerpOrderBookSide(AddressableAccount): return PerpOrderBookSide(account_info, version, meta_data, perp_market_details, bump_index, free_list_len, free_list_head, root_node, leaf_count, nodes) @staticmethod - def parse(context: Context, account_info: AccountInfo, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide": + def parse(account_info: AccountInfo, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide": data = account_info.data if len(data) != layouts.ORDERBOOK_SIDE.sizeof(): raise Exception( @@ -94,7 +94,7 @@ class PerpOrderBookSide(AddressableAccount): account_info = AccountInfo.load(context, address) if account_info is None: raise Exception(f"PerpOrderBookSide account not found at address '{address}'") - return PerpOrderBookSide.parse(context, account_info, perp_market_details) + return PerpOrderBookSide.parse(account_info, perp_market_details) def orders(self) -> typing.Sequence[Order]: if self.leaf_count == 0: diff --git a/mango/orders.py b/mango/orders.py index b60d43d..7f905e8 100644 --- a/mango/orders.py +++ b/mango/orders.py @@ -189,3 +189,76 @@ class Order(typing.NamedTuple): def __repr__(self) -> str: return f"{self}" + + +class OrderBook: + def __init__(self, symbol: str, bids: typing.Sequence[Order], asks: typing.Sequence[Order]): + self.symbol: str = symbol + + # Sort bids high to low, so best bid is at index 0 + bids_list: typing.List[Order] = list(bids) + bids_list.sort(key=lambda order: order.price, reverse=True) + self.bids: typing.Sequence[Order] = bids_list + + # Sort bids low to high, so best bid is at index 0 + asks_list: typing.List[Order] = list(asks) + asks_list.sort(key=lambda order: order.price) + self.asks: typing.Sequence[Order] = asks_list + + # The top bid is the highest price someone is willing to pay to BUY + @property + def top_bid(self) -> typing.Optional[Order]: + if self.bids and len(self.bids) > 0: + # Top-of-book is always at index 0 for us. + return self.bids[0] + return None + + # The top ask is the lowest price someone is willing to pay to SELL + @property + def top_ask(self) -> typing.Optional[Order]: + if self.asks and len(self.asks) > 0: + # Top-of-book is always at index 0 for us. + return self.asks[0] + return None + + # The mid price is halfway between the best bid and best ask. + @property + def mid_price(self) -> typing.Optional[Decimal]: + if self.top_bid is not None and self.top_ask is not None: + return (self.top_bid.price + self.top_ask.price) / 2 + elif self.top_bid is not None: + return self.top_bid.price + elif self.top_ask is not None: + return self.top_ask.price + return None + + @property + def spread(self) -> Decimal: + top_ask = self.top_ask + top_bid = self.top_bid + if top_ask is None or top_bid is None: + return Decimal(0) + else: + return top_ask.price - top_bid.price + + def __str__(self) -> str: + def _order_to_str(order: Order): + quantity = f"{order.quantity:,.8f}" + price = f"{order.price:,.8f}" + return f"{order.side} {quantity:>20} at {price:>20}" + orders_to_show = 5 + lines = [] + for counter in range(orders_to_show): + bid = _order_to_str(self.bids[counter]) if len(self.bids) > counter else "" + ask = _order_to_str(self.asks[counter]) if len(self.asks) > counter else "" + lines += [f"{bid:50} :: {ask}"] + + text = "\n\t".join(lines) + spread_description = "N/A" + if self.spread != 0 and self.top_bid is not None: + spread_percentage = (self.spread / self.top_bid.price) + spread_description = f"{self.spread:,.8f}, {spread_percentage:,.3%}" + return f"Β« π™Ύπš›πšπšŽπš›π™±πš˜πš˜πš” {self.symbol} [spread: {spread_description}]\n\t{text}\nΒ»" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/perpmarket.py b/mango/perpmarket.py index 99ec195..11318c4 100644 --- a/mango/perpmarket.py +++ b/mango/perpmarket.py @@ -54,6 +54,18 @@ class PerpMarket(LoadedMarket): def group(self) -> Group: return self.underlying_perp_market.group + @property + def bids_address(self) -> PublicKey: + return self.underlying_perp_market.bids + + @property + def asks_address(self) -> PublicKey: + return self.underlying_perp_market.asks + + def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: + side: PerpOrderBookSide = PerpOrderBookSide.parse(account_info, self.underlying_perp_market) + return side.orders() + def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]: event_queue: PerpEventQueue = PerpEventQueue.load( context, self.underlying_perp_market.event_queue, self.lot_size_converter) @@ -77,14 +89,6 @@ class PerpMarket(LoadedMarket): distinct.sort(key=lambda address: address._key or [0]) return distinct - def orders(self, context: Context) -> typing.Sequence[Order]: - bids_address: PublicKey = self.underlying_perp_market.bids - asks_address: PublicKey = self.underlying_perp_market.asks - [bids, asks] = AccountInfo.load_multiple(context, [bids_address, asks_address]) - bid_side = PerpOrderBookSide.parse(context, bids, self.underlying_perp_market) - ask_side = PerpOrderBookSide.parse(context, asks, self.underlying_perp_market) - return [*bid_side.orders(), *ask_side.orders()] - def observe_events(self, context: Context, interval: int = 30) -> DisposingSubject: perp_event_queue: PerpEventQueue = PerpEventQueue.load( context, self.underlying_perp_market.event_queue, self.lot_size_converter) diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index 1f4d56e..e3c443a 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -24,7 +24,7 @@ from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .marketoperations import MarketOperations -from .orders import Order +from .orders import Order, OrderBook from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarket import PerpMarket from .wallet import Wallet @@ -86,12 +86,12 @@ class PerpMarketOperations(MarketOperations): def ensure_openorders(self) -> PublicKey: return SYSTEM_PROGRAM_ADDRESS - def load_orders(self) -> typing.Sequence[Order]: - return self.perp_market.orders(self.context) + def load_orderbook(self) -> OrderBook: + return self.perp_market.fetch_orderbook(self.context) def load_my_orders(self) -> typing.Sequence[Order]: - all_orders = self.perp_market.orders(self.context) - return list([o for o in all_orders if o.owner == self.account.address]) + orderbook: OrderBook = self.load_orderbook() + return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == self.account.address]) def __str__(self) -> str: return f"""Β« π™ΏπšŽπš›πš™πšœπ™Ύπš›πšπšŽπš›π™Ώπš•πšŠπšŒπšŽπš› [{self.market_name}] Β»""" diff --git a/mango/serummarket.py b/mango/serummarket.py index d189b05..386073a 100644 --- a/mango/serummarket.py +++ b/mango/serummarket.py @@ -13,7 +13,6 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) -import itertools import typing from decimal import Decimal @@ -44,19 +43,22 @@ class SerumMarket(LoadedMarket): quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size) + @property + def bids_address(self) -> PublicKey: + return self.underlying_serum_market.state.bids() + + @property + def asks_address(self) -> PublicKey: + return self.underlying_serum_market.state.asks() + + def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: + orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data) + return list(map(Order.from_serum_order, orderbook.orders())) + def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue()) return event_queue.unprocessed_events - def orders(self, context: Context) -> typing.Sequence[Order]: - raw_market = self.underlying_serum_market - [bids_info, asks_info] = AccountInfo.load_multiple( - context, [raw_market.state.bids(), raw_market.state.asks()]) - bids_orderbook = PySerumOrderBook.from_bytes(raw_market.state, bids_info.data) - asks_orderbook = PySerumOrderBook.from_bytes(raw_market.state, asks_info.data) - - return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))) - def find_openorders_address_for_owner(self, context: Context, owner: PublicKey) -> typing.Optional[PublicKey]: all_open_orders = OpenOrders.load_for_market_and_owner( context, self.address, owner, context.serum_program_address, self.base.decimals, self.quote.decimals) diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index a1f4529..d32b767 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -23,7 +23,7 @@ from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .marketoperations import MarketOperations -from .orders import Order +from .orders import Order, OrderBook from .serummarket import SerumMarket from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .wallet import Wallet @@ -90,16 +90,16 @@ class SerumMarketOperations(MarketOperations): return self.market_instruction_builder.open_orders_address return self.create_openorders() - def load_orders(self) -> typing.Sequence[Order]: - return self.serum_market.orders(self.context) + def load_orderbook(self) -> OrderBook: + return self.serum_market.fetch_orderbook(self.context) def load_my_orders(self) -> typing.Sequence[Order]: open_orders_address = self.market_instruction_builder.open_orders_address if not open_orders_address: return [] - all_orders = self.serum_market.orders(self.context) - return list([o for o in all_orders if o.owner == open_orders_address]) + orderbook: OrderBook = self.load_orderbook() + return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == open_orders_address]) def _build_crank(self, limit: Decimal = Decimal(32)) -> CombinableInstructions: open_orders_to_crank: typing.List[PublicKey] = [] diff --git a/mango/spotmarket.py b/mango/spotmarket.py index 23107ea..2ac87b8 100644 --- a/mango/spotmarket.py +++ b/mango/spotmarket.py @@ -13,7 +13,6 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) -import itertools import typing from decimal import Decimal @@ -45,19 +44,22 @@ class SpotMarket(LoadedMarket): quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size) + @property + def bids_address(self) -> PublicKey: + return self.underlying_serum_market.state.bids() + + @property + def asks_address(self) -> PublicKey: + return self.underlying_serum_market.state.asks() + + def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: + orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data) + return list(map(Order.from_serum_order, orderbook.orders())) + def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue()) return event_queue.unprocessed_events - def orders(self, context: Context) -> typing.Sequence[Order]: - raw_market = self.underlying_serum_market - [bids_info, asks_info] = AccountInfo.load_multiple( - context, [raw_market.state.bids(), raw_market.state.asks()]) - bids_orderbook = PySerumOrderBook.from_bytes(raw_market.state, bids_info.data) - asks_orderbook = PySerumOrderBook.from_bytes(raw_market.state, asks_info.data) - - return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))) - def __str__(self) -> str: return f"""Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπš {self.symbol} {self.address} [{self.program_address}] Event Queue: {self.underlying_serum_market.state.event_queue()} diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index 75bad04..bc93f83 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -25,7 +25,7 @@ from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .group import Group from .marketoperations import MarketOperations -from .orders import Order +from .orders import Order, OrderBook from .spotmarket import SpotMarket from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .wallet import Wallet @@ -102,15 +102,15 @@ class SpotMarketOperations(MarketOperations): return existing return self.create_openorders() - def load_orders(self) -> typing.Sequence[Order]: - return self.spot_market.orders(self.context) + def load_orderbook(self) -> OrderBook: + return self.spot_market.fetch_orderbook(self.context) def load_my_orders(self) -> typing.Sequence[Order]: if not self.open_orders_address: return [] - all_orders = self.spot_market.orders(self.context) - return list([o for o in all_orders if o.owner == self.open_orders_address]) + orderbook: OrderBook = self.load_orderbook() + return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == self.open_orders_address]) def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions: open_orders_to_crank: typing.List[PublicKey] = [] diff --git a/mango/tradeexecutor.py b/mango/tradeexecutor.py index e0b42a8..9545162 100644 --- a/mango/tradeexecutor.py +++ b/mango/tradeexecutor.py @@ -139,9 +139,11 @@ class ImmediateTradeExecutor(TradeExecutor): def buy(self, symbol: str, quantity: Decimal) -> Order: market_operations: MarketOperations = self._build_market_operations(symbol) - orders = market_operations.load_orders() + orderbook = market_operations.load_orderbook() + if orderbook.top_ask is None: + raise Exception(f"Could not determine top ask on {orderbook.symbol}") - top_ask = min([order.price for order in orders if order.side == Side.SELL]) + top_ask = orderbook.top_ask.price increase_factor = Decimal(1) + self.price_adjustment_factor price = top_ask * increase_factor @@ -152,9 +154,11 @@ class ImmediateTradeExecutor(TradeExecutor): def sell(self, symbol: str, quantity: Decimal) -> Order: market_operations: MarketOperations = self._build_market_operations(symbol) - orders = market_operations.load_orders() + orderbook = market_operations.load_orderbook() + if orderbook.top_bid is None: + raise Exception(f"Could not determine top bid on {orderbook.symbol}") - top_bid = max([order.price for order in orders if order.side == Side.BUY]) + top_bid = orderbook.top_bid.price decrease_factor = Decimal(1) - self.price_adjustment_factor price = top_bid * decrease_factor diff --git a/mango/watchers.py b/mango/watchers.py index 224cc98..185d0f3 100644 --- a/mango/watchers.py +++ b/mango/watchers.py @@ -18,7 +18,6 @@ import typing from decimal import Decimal from pyserum.market import Market as PySerumMarket -from pyserum.market.orderbook import OrderBook as PySerumOrderBook from solana.publickey import PublicKey from .account import Account @@ -30,13 +29,13 @@ from .group import Group from .healthcheck import HealthCheck from .instructions import build_create_serum_open_orders_instructions from .inventory import Inventory, InventorySource +from .loadedmarket import LoadedMarket from .market import Market from .observables import DisposePropagator, LatestItemObserverSubscriber from .openorders import OpenOrders from .oracle import Price from .oraclefactory import OracleProvider, create_oracle_provider -from .orderbookside import OrderBookSideType, PerpOrderBookSide -from .orders import Order +from .orders import OrderBook from .perpmarket import PerpMarket from .placedorder import PlacedOrdersContainer from .serummarket import SerumMarket @@ -205,45 +204,37 @@ def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscripti return LamdaUpdateWatcher(serum_inventory_accessor) -def build_perp_orderbook_side_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, perp_market: PerpMarket, side: OrderBookSideType) -> Watcher[typing.Sequence[Order]]: - orderbook_address: PublicKey = perp_market.underlying_perp_market.bids if side == OrderBookSideType.BIDS else perp_market.underlying_perp_market.asks - orderbook_side_info = AccountInfo.load(context, orderbook_address) - if orderbook_side_info is None: - raise Exception(f"Could not find perp order book side at address {orderbook_address}.") - initial_orderbook_side: PerpOrderBookSide = PerpOrderBookSide.parse( - context, orderbook_side_info, perp_market.underlying_perp_market) +def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, market: LoadedMarket) -> Watcher[OrderBook]: + orderbook_addresses: typing.List[PublicKey] = [ + market.bids_address, + market.asks_address + ] + orderbook_infos = AccountInfo.load_multiple(context, orderbook_addresses) + if len(orderbook_infos) != 2 or orderbook_infos[0] is None or orderbook_infos[1] is None: + raise Exception(f"Could not find {market.symbol} order book at addresses {orderbook_addresses}.") - orders_subscription = WebSocketAccountSubscription[typing.Sequence[Order]]( - context, orderbook_address, lambda account_info: PerpOrderBookSide.parse(context, account_info, perp_market.underlying_perp_market).orders()) - manager.add(orders_subscription) + initial_orderbook: OrderBook = market.parse_account_infos_to_orderbook(orderbook_infos[0], orderbook_infos[1]) + updatable_orderbook: OrderBook = market.parse_account_infos_to_orderbook( + orderbook_infos[0], orderbook_infos[1]) - latest_orders_observer = LatestItemObserverSubscriber[typing.Sequence[Order]](initial_orderbook_side.orders()) + def _update_bids(account_info: AccountInfo) -> OrderBook: + new_bids = market.parse_account_info_to_orders(account_info) + updatable_orderbook.bids = new_bids + return updatable_orderbook - orders_subscription.publisher.subscribe(latest_orders_observer) - health_check.add("orderbook_side_subscription", orders_subscription.publisher) - return latest_orders_observer + def _update_asks(account_info: AccountInfo) -> OrderBook: + new_asks = market.parse_account_info_to_orders(account_info) + updatable_orderbook.asks = new_asks + return updatable_orderbook + bids_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[0], _update_bids) + manager.add(bids_subscription) + asks_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[1], _update_asks) + manager.add(asks_subscription) + orderbook_observer = LatestItemObserverSubscriber[OrderBook](initial_orderbook) -def build_serum_orderbook_side_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, underlying_serum_market: PySerumMarket, side: OrderBookSideType) -> Watcher[typing.Sequence[Order]]: - orderbook_address: PublicKey = underlying_serum_market.state.bids( - ) if side == OrderBookSideType.BIDS else underlying_serum_market.state.asks() - orderbook_side_info = AccountInfo.load(context, orderbook_address) - if orderbook_side_info is None: - raise Exception(f"Could not find Serum order book side at address {orderbook_address}.") - - def account_info_to_orderbook(account_info: AccountInfo) -> typing.Sequence[Order]: - serum_orderbook_side = PySerumOrderBook.from_bytes( - underlying_serum_market.state, account_info.data) - return list(map(Order.from_serum_order, serum_orderbook_side.orders())) - - initial_orderbook_side: typing.Sequence[Order] = account_info_to_orderbook(orderbook_side_info) - - orders_subscription = WebSocketAccountSubscription[typing.Sequence[Order]]( - context, orderbook_address, account_info_to_orderbook) - manager.add(orders_subscription) - - latest_orders_observer = LatestItemObserverSubscriber[typing.Sequence[Order]](initial_orderbook_side) - - orders_subscription.publisher.subscribe(latest_orders_observer) - health_check.add("orderbook_side_subscription", orders_subscription.publisher) - return latest_orders_observer + bids_subscription.publisher.subscribe(orderbook_observer) + asks_subscription.publisher.subscribe(orderbook_observer) + health_check.add("orderbook_bids_subscription", bids_subscription.publisher) + health_check.add("orderbook_asks_subscription", asks_subscription.publisher) + return orderbook_observer diff --git a/tests/fakes.py b/tests/fakes.py index 0a3cdb8..221ef17 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -144,11 +144,11 @@ def fake_inventory(): def fake_bids(): - return None + return [] def fake_asks(): - return None + return [] def fake_model_state(order_owner: typing.Optional[PublicKey] = None, @@ -158,8 +158,7 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None, price: typing.Optional[mango.Price] = None, placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, inventory: typing.Optional[mango.Inventory] = None, - bids: typing.Optional[typing.Sequence[mango.Order]] = None, - asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.ModelState: + orderbook: typing.Optional[mango.OrderBook] = None) -> mango.ModelState: order_owner = order_owner or fake_seeded_public_key("order owner") market = market or fake_loaded_market() group = group or fake_group() @@ -167,17 +166,15 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None, price = price or fake_price() placed_orders_container = placed_orders_container or fake_placed_orders_container() inventory = inventory or fake_inventory() - bids = bids or fake_bids() - asks = asks or fake_asks() + orderbook = orderbook or mango.OrderBook("FAKE", fake_bids(), fake_asks()) group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) placed_orders_container_watcher: mango.ManualUpdateWatcher[ mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) - bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids) - asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks) + orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook) return mango.ModelState(order_owner, market, group_watcher, account_watcher, price_watcher, placed_orders_container_watcher, - inventory_watcher, bids_watcher, asks_watcher) + inventory_watcher, orderbook_watcher) diff --git a/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py b/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py index e1560e5..826c263 100644 --- a/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py +++ b/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py @@ -25,7 +25,8 @@ asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) ] -model_state = fake_model_state(bids=bids, asks=asks) +orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks) +model_state = fake_model_state(orderbook=orderbook) def test_from_args(): @@ -73,7 +74,8 @@ def test_accumulation_ignores_own_orders_updated(): fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) ] - model_state = fake_model_state(order_owner=order_owner, bids=bids, asks=asks) + orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks) + model_state = fake_model_state(order_owner=order_owner, orderbook=orderbook) context = fake_context() buy: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(6), side=mango.Side.BUY) sell: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) diff --git a/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py b/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py index 2c642f1..5f70e2c 100644 --- a/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py +++ b/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py @@ -13,7 +13,9 @@ top_bid: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY, order_ # The top ask is the lowest price someone is willing to pay to SELL top_ask: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) -model_state = fake_model_state(market=fake_loaded_market(), bids=[top_bid], asks=[top_ask]) +orderbook: mango.OrderBook = mango.OrderBook("TEST", [top_bid], [top_ask]) + +model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) def test_from_args(): @@ -77,7 +79,8 @@ def test_bid_too_high_no_bid_results_in_new_bid(): order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[top_ask]) + orderbook: mango.OrderBook = mango.OrderBook("TEST", [], [top_ask]) + model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) @@ -89,7 +92,8 @@ def test_ask_too_low_no_ask_results_in_new_ask(): order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - model_state = fake_model_state(market=fake_loaded_market(), bids=[top_bid], asks=[]) + orderbook: mango.OrderBook = mango.OrderBook("TEST", [top_bid], []) + model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) assert result[0].price == 91 @@ -100,7 +104,8 @@ def test_ask_no_orderbook_results_in_no_change(): order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[]) + orderbook: mango.OrderBook = mango.OrderBook("TEST", [], []) + model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) assert result == [order] @@ -111,7 +116,8 @@ def test_bid_no_orderbook_results_in_no_change(): order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[]) + orderbook: mango.OrderBook = mango.OrderBook("TEST", [], []) + model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) assert result == [order]