From 8c425375724b76457bea0db3fd8f24c470ee8a84 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 9 Sep 2021 14:10:34 +0100 Subject: [PATCH] Updated SpotCollateralCalculator to take tokens in OpenOrders into account. --- mango/collateralcalculator.py | 19 +++++++--- mango/inventory.py | 11 ++++-- mango/marketmaking/modelstatebuilder.py | 37 ++++++++++++++----- .../marketmaking/modelstatebuilderfactory.py | 27 +++++++++++--- mango/watchers.py | 4 +- 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/mango/collateralcalculator.py b/mango/collateralcalculator.py index 3cd78e0..42da7dd 100644 --- a/mango/collateralcalculator.py +++ b/mango/collateralcalculator.py @@ -21,6 +21,7 @@ from decimal import Decimal from .account import Account from .cache import Cache, PriceCache +from .openorders import OpenOrders from .spotmarketinfo import SpotMarketInfo from .tokenvalue import TokenValue @@ -29,7 +30,7 @@ class CollateralCalculator(metaclass=abc.ABCMeta): def __init__(self): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) - def calculate(self, account: Account, cache: Cache) -> TokenValue: + def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue: raise NotImplementedError("CollateralCalculator.calculate() is not implemented on the base type.") @@ -37,7 +38,7 @@ class SerumCollateralCalculator(CollateralCalculator): def __init__(self): super().__init__() - def calculate(self, account: Account, cache: Cache) -> TokenValue: + def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue: raise NotImplementedError("SerumCollateralCalculator.calculate() is not implemented.") @@ -54,7 +55,7 @@ class SpotCollateralCalculator(CollateralCalculator): # Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056): # yes I think we ignore perps # - def calculate(self, account: Account, cache: Cache) -> TokenValue: + def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue: # Quote token calculation: # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. @@ -70,12 +71,18 @@ class SpotCollateralCalculator(CollateralCalculator): raise Exception( f"Could not read spot market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") + in_orders: Decimal = Decimal(0) + if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in all_open_orders: + open_orders: OpenOrders = all_open_orders[str(basket_token.spot_open_orders)] + in_orders = open_orders.quote_token_total + \ + (open_orders.base_token_total * token_price.price * spot_market.init_asset_weight) + # Base token calculations: # total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. - weighted: Decimal = token_price.price * (( + weighted: Decimal = in_orders + (token_price.price * (( basket_token.deposit.value * spot_market.init_asset_weight) - ( - basket_token.borrow.value * spot_market.init_liab_weight)) + basket_token.borrow.value * spot_market.init_liab_weight))) total += weighted return TokenValue(account.group.shared_quote_token.token, total) @@ -94,7 +101,7 @@ class PerpCollateralCalculator(CollateralCalculator): # Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056): # yes I think we ignore perps # - def calculate(self, account: Account, cache: Cache) -> TokenValue: + def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue: # Quote token calculation: # total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index. diff --git a/mango/inventory.py b/mango/inventory.py index 88b17b0..9b99646 100644 --- a/mango/inventory.py +++ b/mango/inventory.py @@ -15,6 +15,7 @@ import logging +import typing from decimal import Decimal @@ -23,6 +24,7 @@ from .cache import Cache from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator from .group import Group from .market import InventorySource, Market +from .openorders import OpenOrders from .perpmarket import PerpMarket from .tokenvalue import TokenValue from .watcher import Watcher @@ -56,8 +58,9 @@ class Inventory: class SpotInventoryAccountWatcher: - def __init__(self, market: Market, account_watcher: Watcher[Account], cache_watcher: Watcher[Cache]): + def __init__(self, market: Market, account_watcher: Watcher[Account], all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]], cache_watcher: Watcher[Cache]): self.account_watcher: Watcher[Account] = account_watcher + self.all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]] = all_open_orders_watchers self.cache_watcher: Watcher[Cache] = cache_watcher account: Account = account_watcher.latest base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol) @@ -75,7 +78,9 @@ class SpotInventoryAccountWatcher: mngo = account.group.find_token_info_by_symbol("MNGO").token mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0)) - available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + all_open_orders: typing.Dict[str, OpenOrders] = { + str(oo_watcher.latest.address): oo_watcher.latest for oo_watcher in self.all_open_orders_watchers} + available_collateral: TokenValue = self.collateral_calculator.calculate(account, all_open_orders, cache) base_value = account.net_assets[self.base_index] if base_value is None: @@ -109,7 +114,7 @@ class PerpInventoryAccountWatcher: raise Exception( f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}.") - available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + available_collateral: TokenValue = self.collateral_calculator.calculate(account, {}, cache) base_lots = perp_account.base_position base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots) diff --git a/mango/marketmaking/modelstatebuilder.py b/mango/marketmaking/modelstatebuilder.py index a847a16..5e7b986 100644 --- a/mango/marketmaking/modelstatebuilder.py +++ b/mango/marketmaking/modelstatebuilder.py @@ -185,7 +185,8 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): group_address: PublicKey, cache_address: PublicKey, account_address: PublicKey, - open_orders_address: PublicKey + open_orders_address: PublicKey, + all_open_orders_addresses: typing.Sequence[PublicKey] ): super().__init__() self.order_owner: PublicKey = order_owner @@ -196,6 +197,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.cache_address: PublicKey = cache_address self.account_address: PublicKey = account_address self.open_orders_address: PublicKey = open_orders_address + self.all_open_orders_addresses: typing.Sequence[PublicKey] = all_open_orders_addresses self.collateral_calculator: mango.CollateralCalculator = mango.SpotCollateralCalculator() @@ -204,16 +206,33 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.group_address, self.cache_address, self.account_address, - self.open_orders_address, self.market.underlying_serum_market.state.bids(), - self.market.underlying_serum_market.state.asks() + self.market.underlying_serum_market.state.asks(), + *self.all_open_orders_addresses ] account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) group: mango.Group = mango.Group.parse(context, account_infos[0]) cache: mango.Cache = mango.Cache.parse(account_infos[1]) account: mango.Account = mango.Account.parse(account_infos[2], group) - placed_orders_container: mango.PlacedOrdersContainer = mango.OpenOrders.parse( - account_infos[3], self.market.base.decimals, self.market.quote.decimals) + + # Update our stash of OpenOrders addresses for next time, in case new OpenOrders accounts were added + self.all_open_orders_addresses = list([oo for oo in account.spot_open_orders if oo is not None]) + + spot_open_orders_account_infos_by_address = { + str(account_info.address): account_info for account_info in account_infos[5:]} + + all_open_orders: typing.Dict[str, mango.OpenOrders] = {} + for basket_token in account.basket: + if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in spot_open_orders_account_infos_by_address: + account_info: mango.AccountInfo = spot_open_orders_account_infos_by_address[str( + basket_token.spot_open_orders)] + open_orders: mango.OpenOrders = mango.OpenOrders.parse( + account_info, + basket_token.token_info.decimals, + account.shared_quote_token.token_info.token.decimals) + all_open_orders[str(basket_token.spot_open_orders)] = open_orders + + placed_orders_container: mango.PlacedOrdersContainer = all_open_orders[str(self.open_orders_address)] # Spot markets don't accrue MNGO liquidity incentives mngo = account.group.find_token_info_by_symbol("MNGO").token @@ -222,7 +241,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): base_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.base.symbol) quote_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol) - available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + available_collateral: TokenValue = self.collateral_calculator.calculate(account, all_open_orders, cache) inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, mngo_accrued, available_collateral, @@ -230,9 +249,9 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): quote_value) bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[4], self.market.underlying_serum_market) + account_infos[3], self.market.underlying_serum_market) asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( - account_infos[5], self.market.underlying_serum_market) + account_infos[4], self.market.underlying_serum_market) price: mango.Price = self.oracle.fetch_price(context) @@ -289,7 +308,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots) base_token_value = mango.TokenValue(self.market.base, base_value) quote_token_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol) - available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache) + available_collateral: TokenValue = self.collateral_calculator.calculate(account, {}, cache) inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, perp_account.mngo_accrued, available_collateral, diff --git a/mango/marketmaking/modelstatebuilderfactory.py b/mango/marketmaking/modelstatebuilderfactory.py index 7a30a11..0f57ff6 100644 --- a/mango/marketmaking/modelstatebuilderfactory.py +++ b/mango/marketmaking/modelstatebuilderfactory.py @@ -14,12 +14,12 @@ # [Email](mailto:hello@blockworks.foundation) import enum -from mango.constants import SYSTEM_PROGRAM_ADDRESS import mango import typing from solana.publickey import PublicKey +from ..constants import SYSTEM_PROGRAM_ADDRESS from .modelstate import ModelState from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder @@ -89,11 +89,13 @@ def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango oracle: mango.Oracle) -> ModelStateBuilder: market_index: int = group.find_spot_market_index(market.address) open_orders_address: typing.Optional[PublicKey] = account.spot_open_orders[market_index] + all_open_orders_addresses: typing.Sequence[PublicKey] = list( + [oo for oo in account.spot_open_orders if oo is not None]) if open_orders_address is None: raise Exception( f"Could not find spot openorders in account {account.address} for market {market.symbol}.") return SpotPollingModelStateBuilder( - open_orders_address, market, oracle, group.address, group.cache, account.address, open_orders_address) + open_orders_address, market, oracle, group.address, group.cache, account.address, open_orders_address, all_open_orders_addresses) def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket, @@ -137,9 +139,24 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man cache: mango.Cache = mango.Cache.load(context, group.cache) cache_watcher: mango.Watcher[mango.Cache] = mango.build_cache_watcher( context, websocket_manager, health_check, cache, group) - inventory_watcher = mango.SpotInventoryAccountWatcher(market, latest_account_observer, cache_watcher) - latest_open_orders_observer = mango.build_spot_open_orders_watcher( - context, websocket_manager, health_check, wallet, account, group, market) + + all_open_orders_watchers: typing.List[mango.Watcher[mango.OpenOrders]] = [] + for basket_token in account.basket: + if basket_token.spot_open_orders is not None: + spot_market_symbol: str = f"spot:{basket_token.token_info.token.symbol}/{account.shared_quote_token.token_info.token.symbol}" + spot_market = context.market_lookup.find_by_symbol(spot_market_symbol) + if spot_market is None: + raise Exception(f"Could not find spot market {spot_market_symbol}") + if not isinstance(spot_market, mango.SpotMarket): + raise Exception(f"Market {spot_market_symbol} is not a spot market") + oo_watcher = mango.build_spot_open_orders_watcher( + context, websocket_manager, health_check, wallet, account, group, spot_market) + all_open_orders_watchers += [oo_watcher] + if market.base == spot_market.base and market.quote == spot_market.quote: + latest_open_orders_observer = oo_watcher + + inventory_watcher = mango.SpotInventoryAccountWatcher( + market, latest_account_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( diff --git a/mango/watchers.py b/mango/watchers.py index e8f9c43..b19392a 100644 --- a/mango/watchers.py +++ b/mango/watchers.py @@ -81,7 +81,7 @@ def build_cache_watcher(context: Context, manager: WebSocketSubscriptionManager, return latest_cache_observer -def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[PlacedOrdersContainer]: +def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[OpenOrders]: market_index = group.find_spot_market_index(spot_market.address) open_orders_address = account.spot_open_orders[market_index] if open_orders_address is None: @@ -97,7 +97,7 @@ def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscript manager.add(spot_open_orders_subscription) initial_spot_open_orders = OpenOrders.load( context, open_orders_address, spot_market.base.decimals, spot_market.quote.decimals) - latest_open_orders_observer = LatestItemObserverSubscriber[PlacedOrdersContainer]( + latest_open_orders_observer = LatestItemObserverSubscriber[OpenOrders]( initial_spot_open_orders) spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer) health_check.add("open_orders_subscription", spot_open_orders_subscription.publisher)